|
2888
|
116
|
9
|
2026-05-07T11:46:57.924184+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778154417924_m2.jpg...
|
PhpStorm
|
faVsco.js – ~/jiminny/app/vendor/hubspot/api-clien faVsco.js – ~/jiminny/app/vendor/hubspot/api-client/codegen/Crm/Deals/Api/BasicApi.php...
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
Editor for custom.log
Code changed:
Hide
Sync Changes
Hide This Notification
Reader Mode
<?php
/**
* BasicApi
* PHP version 7.3
*
* @category Class
* @package HubSpot\Client\Crm\Deals
* @author OpenAPI Generator team
* @link [URL_WITH_CREDENTIALS] Class
* @package HubSpot\Client\Crm\Deals
* @author OpenAPI Generator team
* @link [URL_WITH_CREDENTIALS] int $hostIndex Host index (required)
*/
public function setHostIndex($hostIndex): void
{
$this->hostIndex = $hostIndex;
}
/**
* Get the host index
*
* @return int Host index
*/
public function getHostIndex()
{
return $this->hostIndex;
}
/**
* @return Configuration
*/
public function getConfig()
{
return $this->config;
}
/**
* Operation archive
*
* Archive
*
* @param string $deal_id deal_id (required)
*
* @throws \HubSpot\Client\Crm\Deals\ApiException on non-2xx response
* @throws \InvalidArgumentException
* @return void
*/
public function archive($deal_id)
{
$this->archiveWithHttpInfo($deal_id);
}
/**
* Operation archiveWithHttpInfo
*
* Archive
*
* @param string $deal_id (required)
*
* @throws \HubSpot\Client\Crm\Deals\ApiException on non-2xx response
* @throws \InvalidArgumentException
* @return array of null, HTTP status code, HTTP response headers (array of strings)
*/
public function archiveWithHttpInfo($deal_id)
{
$request = $this->archiveRequest($deal_id);
try {
$options = $this->createHttpClientOption();
try {
$response = $this->client->send($request, $options);
} catch (RequestException $e) {
throw new ApiException(
"[{$e->getCode()}] {$e->getMessage()}",
(int) $e->getCode(),
$e->getResponse() ? $e->getResponse()->getHeaders() : null,
$e->getResponse() ? (string) $e->getResponse()->getBody() : null
);
} catch (ConnectException $e) {
throw new ApiException(
"[{$e->getCode()}] {$e->getMessage()}",
(int) $e->getCode(),
null,
null
);
}
$statusCode = $response->getStatusCode();
if ($statusCode < 200 || $statusCode > 299) {
throw new ApiException(
sprintf(
'[%d] Error connecting to the API (%s)',
$statusCode,
(string) $request->getUri()
),
$statusCode,
$response->getHeaders(),
(string) $response->getBody()
);
}
return [null, $statusCode, $response->getHeaders()];
} catch (ApiException $e) {
switch ($e->getCode()) {
default:
$data = ObjectSerializer::deserialize(
$e->getResponseBody(),
'\HubSpot\Client\Crm\Deals\Model\Error',
$e->getResponseHeaders()
);
$e->setResponseObject($data);
break;
}
throw $e;
}
}
/**
* Operation archiveAsync
*
* Archive
*
* @param string $deal_id (required)
*
* @throws \InvalidArgumentException
* @return \GuzzleHttp\Promise\PromiseInterface
*/
public function archiveAsync($deal_id)
{
return $this->archiveAsyncWithHttpInfo($deal_id)
->then(
function ($response) {
return $response[0];
}
);
}
/**
* Operation archiveAsyncWithHttpInfo
*
* Archive
*
* @param string $deal_id (required)
*
* @throws \InvalidArgumentException
* @return \GuzzleHttp\Promise\PromiseInterface
*/
public function archiveAsyncWithHttpInfo($deal_id)
{
$returnType = '';
$request = $this->archiveRequest($deal_id);
return $this->client
->sendAsync($request, $this->createHttpClientOption())
->then(
function ($response) use ($returnType) {
return [null, $response->getStatusCode(), $response->getHeaders()];
},
function ($exception) {
$response = $exception->getResponse();
$statusCode = $response->getStatusCode();
throw new ApiException(
sprintf(
'[%d] Error connecting to the API (%s)',
$statusCode,
$exception->getRequest()->getUri()
),
$statusCode,
$response->getHeaders(),
(string) $response->getBody()
);
}
);
}
/**
* Create request for operation 'archive'
*
* @param string $deal_id (required)
*
* @throws \InvalidArgumentException
* @return \GuzzleHttp\Psr7\Request
*/
public function archiveRequest($deal_id)
{
// verify the required parameter 'deal_id' is set
if ($deal_id === null || (is_array($deal_id) && count($deal_id) === 0)) {
throw new \InvalidArgumentException(
'Missing the required parameter $deal_id when calling archive'
);
}
$resourcePath = '/crm/v3/objects/deals/{dealId}';
$formParams = [];
$queryParams = [];
$headerParams = [];
$httpBody = '';
$multipart = false;
// path params
if ($deal_id !== null) {
$resourcePath = str_replace(
'{' . 'dealId' . '}',
ObjectSerializer::toPathValue($deal_id),
$resourcePath
);
}
if ($multipart) {
$headers = $this->headerSelector->selectHeadersForMultipart(
['*/*']
);
} else {
$headers = $this->headerSelector->selectHeaders(
['*/*'],
[]
);
}
// for model (json/xml)
if (count($formParams) > 0) {
if ($multipart) {
$multipartContents = [];
foreach ($formParams as $formParamName => $formParamValue) {
$formParamValueItems = is_array($formParamValue) ? $formParamValue : [$formParamValue];
foreach ($formParamValueItems as $formParamValueItem) {
$multipartContents[] = [
'name' => $formParamName,
'contents' => $formParamValueItem
];
}
}
// for HTTP post (form)
$httpBody = new MultipartStream($multipartContents);
} elseif ($headers['Content-Type'] === 'application/json') {
$httpBody = \GuzzleHttp\json_encode($formParams);
} else {
// for HTTP post (form)
$httpBody = \GuzzleHttp\Psr7\Query::build($formParams);
}
}
// this endpoint requires API key authentication
$apiKey = $this->config->getApiKeyWithPrefix('hapikey');
if ($apiKey !== null) {
$queryParams['hapikey'] = $apiKey;
}
// this endpoint requires OAuth (access token)
if ($this->config->getAccessToken() !== null) {
$headers['Authorization'] = 'Bearer ' . $this->config->getAccessToken();
}
$defaultHeaders = [];
if ($this->config->getUserAgent()) {
$defaultHeaders['User-Agent'] = $this->config->getUserAgent();
}
$headers = array_merge(
$defaultHeaders,
$headerParams,
$headers
);
$query = \GuzzleHttp\Psr7\Query::build($queryParams);
return new Request(
'DELETE',
$this->config->getHost() . $resourcePath . ($query ? "?{$query}" : ''),
$headers,
$httpBody
);
}
/**
* Operation create
*
* Create
*
* @param \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectInput $simple_public_object_input simple_public_object_input (required)
*
* @throws \HubSpot\Client\Crm\Deals\ApiException on non-2xx response
* @throws \InvalidArgumentException
* @return \HubSpot\Client\Crm\Deals\Model\SimplePublicObject|\HubSpot\Client\Crm\Deals\Model\Error
*/
public function create($simple_public_object_input)
{
list($response) = $this->createWithHttpInfo($simple_public_object_input);
return $response;
}
/**
* Operation createWithHttpInfo
*
* Create
*
* @param \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectInput $simple_public_object_input (required)
*
* @throws \HubSpot\Client\Crm\Deals\ApiException on non-2xx response
* @throws \InvalidArgumentException
* @return array of \HubSpot\Client\Crm\Deals\Model\SimplePublicObject|\HubSpot\Client\Crm\Deals\Model\Error, HTTP status code, HTTP response headers (array of strings)
*/
public function createWithHttpInfo($simple_public_object_input)
{
$request = $this->createRequest($simple_public_object_input);
try {
$options = $this->createHttpClientOption();
try {
$response = $this->client->send($request, $options);
} catch (RequestException $e) {
throw new ApiException(
"[{$e->getCode()}] {$e->getMessage()}",
(int) $e->getCode(),
$e->getResponse() ? $e->getResponse()->getHeaders() : null,
$e->getResponse() ? (string) $e->getResponse()->getBody() : null
);
} catch (ConnectException $e) {
throw new ApiException(
"[{$e->getCode()}] {$e->getMessage()}",
(int) $e->getCode(),
null,
null
);
}
$statusCode = $response->getStatusCode();
if ($statusCode < 200 || $statusCode > 299) {
throw new ApiException(
sprintf(
'[%d] Error connecting to the API (%s)',
$statusCode,
(string) $request->getUri()
),
$statusCode,
$response->getHeaders(),
(string) $response->getBody()
);
}
switch($statusCode) {
case 201:
if ('\HubSpot\Client\Crm\Deals\Model\SimplePublicObject' === '\SplFileObject') {
$content = $response->getBody(); //stream goes to serializer
} else {
$content = (string) $response->getBody();
}
return [
ObjectSerializer::deserialize($content, '\HubSpot\Client\Crm\Deals\Model\SimplePublicObject', []),
$response->getStatusCode(),
$response->getHeaders()
];
default:
if ('\HubSpot\Client\Crm\Deals\Model\Error' === '\SplFileObject') {
$content = $response->getBody(); //stream goes to serializer
} else {
$content = (string) $response->getBody();
}
return [
ObjectSerializer::deserialize($content, '\HubSpot\Client\Crm\Deals\Model\Error', []),
$response->getStatusCode(),
$response->getHeaders()
];
}
$returnType = '\HubSpot\Client\Crm\Deals\Model\SimplePublicObject';
if ($returnType === '\SplFileObject') {
$content = $response->getBody(); //stream goes to serializer
} else {
$content = (string) $response->getBody();
}
return [
ObjectSerializer::deserialize($content, $returnType, []),
$response->getStatusCode(),
$response->getHeaders()
];
} catch (ApiException $e) {
switch ($e->getCode()) {
case 201:
$data = ObjectSerializer::deserialize(
$e->getResponseBody(),
'\HubSpot\Client\Crm\Deals\Model\SimplePublicObject',
$e->getResponseHeaders()
);
$e->setResponseObject($data);
break;
default:
$data = ObjectSerializer::deserialize(
$e->getResponseBody(),
'\HubSpot\Client\Crm\Deals\Model\Error',
$e->getResponseHeaders()
);
$e->setResponseObject($data);
break;
}
throw $e;
}
}
/**
* Operation createAsync
*
* Create
*
* @param \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectInput $simple_public_object_input (required)
*
* @throws \InvalidArgumentException
* @return \GuzzleHttp\Promise\PromiseInterface
*/
public function createAsync($simple_public_object_input)
{
return $this->createAsyncWithHttpInfo($simple_public_object_input)
->then(
function ($response) {
return $response[0];
}
);
}
/**
* Operation createAsyncWithHttpInfo
*
* Create
*
* @param \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectInput $simple_public_object_input (required)
*
* @throws \InvalidArgumentException
* @return \GuzzleHttp\Promise\PromiseInterface
*/
public function createAsyncWithHttpInfo($simple_public_object_input)
{
$returnType = '\HubSpot\Client\Crm\Deals\Model\SimplePublicObject';
$request = $this->createRequest($simple_public_object_input);
return $this->client
->sendAsync($request, $this->createHttpClientOption())
->then(
function ($response) use ($returnType) {
if ($returnType === '\SplFileObject') {
$content = $response->getBody(); //stream goes to serializer
} else {
$content = (string) $response->getBody();
}
return [
ObjectSerializer::deserialize($content, $returnType, []),
$response->getStatusCode(),
$response->getHeaders()
];
},
function ($exception) {
$response = $exception->getResponse();
$statusCode = $response->getStatusCode();
throw new ApiException(
sprintf(
'[%d] Error connecting to the API (%s)',
$statusCode,
$exception->getRequest()->getUri()
),
$statusCode,
$response->getHeaders(),
(string) $response->getBody()
);
}
);
}
/**
* Create request for operation 'create'
*
* @param \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectInput $simple_public_object_input (required)
*
* @throws \InvalidArgumentException
* @return \GuzzleHttp\Psr7\Request
*/
public function createRequest($simple_public_object_input)
{
// verify the required parameter 'simple_public_object_input' is set
if ($simple_public_object_input === null || (is_array($simple_public_object_input) && count($simple_public_object_input) === 0)) {
throw new \InvalidArgumentException(
'Missing the required parameter $simple_public_object_input when calling create'
);
}
$resourcePath = '/crm/v3/objects/deals';
$formParams = [];
$queryParams = [];
$headerParams = [];
$httpBody = '';
$multipart = false;
if ($multipart) {
$headers = $this->headerSelector->selectHeadersForMultipart(
['application/json', '*/*']
);
} else {
$headers = $this->headerSelector->selectHeaders(
['application/json', '*/*'],
['application/json']
);
}
// for model (json/xml)
if (isset($simple_public_object_input)) {
if ($headers['Content-Type'] === 'application/json') {
$httpBody = \GuzzleHttp\json_encode(ObjectSerializer::sanitizeForSerialization($simple_public_object_input));
} else {
$httpBody = $simple_public_object_input;
}
} elseif (count($formParams) > 0) {
if ($multipart) {
$multipartContents = [];
foreach ($formParams as $formParamName => $formParamValue) {
$formParamValueItems = is_array($formParamValue) ? $formParamValue : [$formParamValue];
foreach ($formParamValueItems as $formParamValueItem) {
$multipartContents[] = [
'name' => $formParamName,
'contents' => $formParamValueItem
];
}
}
// for HTTP post (form)
$httpBody = new MultipartStream($multipartContents);
} elseif ($headers['Content-Type'] === 'application/json') {
$httpBody = \GuzzleHttp\json_encode($formParams);
} else {
// for HTTP post (form)
$httpBody = \GuzzleHttp\Psr7\Query::build($formParams);
}
}
// this endpoint requires API key authentication
$apiKey = $this->config->getApiKeyWithPrefix('hapikey');
if ($apiKey !== null) {
$queryParams['hapikey'] = $apiKey;
}
// this endpoint requires OAuth (access token)
if ($this->config->getAccessToken() !== null) {
$headers['Authorization'] = 'Bearer ' . $this->config->getAccessToken();
}
$defaultHeaders = [];
if ($this->config->getUserAgent()) {
$defaultHeaders['User-Agent'] = $this->config->getUserAgent();
}
$headers = array_merge(
$defaultHeaders,
$headerParams,
$headers
);
$query = \GuzzleHttp\Psr7\Query::build($queryParams);
return new Request(
'POST',
$this->config->getHost() . $resourcePath . ($query ? "?{$query}" : ''),
$headers,
$httpBody
);
}
/**
* Operation getById
*
* Read
*
* @param string $deal_id deal_id (required)
* @param string[] $properties A comma separated list of the properties to be returned in the response. If any of the specified properties are not present on the requested object(s), they will be ignored. (optional)
* @param string[] $associations A comma separated list of object types to retrieve associated IDs for. If any of the specified associations do not exist, they will be ignored. (optional)
* @param bool $archived Whether to return only results that have been archived. (optional, default to false)
* @param string $id_property The name of a property whose values are unique for this object type (optional)
*
* @throws \HubSpot\Client\Crm\Deals\ApiException on non-2xx response
* @throws \InvalidArgumentException
* @return \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations|\HubSpot\Client\Crm\Deals\Model\Error
*/
public function getById($deal_id, $properties = null, $associations = null, $archived = false, $id_property = null)
{
list($response) = $this->getByIdWithHttpInfo($deal_id, $properties, $associations, $archived, $id_property);
return $response;
}
/**
* Operation getByIdWithHttpInfo
*
* Read
*
* @param string $deal_id (required)
* @param string[] $properties A comma separated list of the properties to be returned in the response. If any of the specified properties are not present on the requested object(s), they will be ignored. (optional)
* @param string[] $associations A comma separated list of object types to retrieve associated IDs for. If any of the specified associations do not exist, they will be ignored. (optional)
* @param bool $archived Whether to return only results that have been archived. (optional, default to false)
* @param string $id_property The name of a property whose values are unique for this object type (optional)
*
* @throws \HubSpot\Client\Crm\Deals\ApiException on non-2xx response
* @throws \InvalidArgumentException
* @return array of \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations|\HubSpot\Client\Crm\Deals\Model\Error, HTTP status code, HTTP response headers (array of strings)
*/
public function getByIdWithHttpInfo($deal_id, $properties = null, $associations = null, $archived = false, $id_property = null)
{
$request = $this->getByIdRequest($deal_id, $properties, $associations, $archived, $id_property);
try {
$options = $this->createHttpClientOption();
try {
$response = $this->client->send($request, $options);
} catch (RequestException $e) {
throw new ApiException(
"[{$e->getCode()}] {$e->getMessage()}",
(int) $e->getCode(),
$e->getResponse() ? $e->getResponse()->getHeaders() : null,
$e->getResponse() ? (string) $e->getResponse()->getBody() : null
);
} catch (ConnectException $e) {
throw new ApiException(
"[{$e->getCode()}] {$e->getMessage()}",
(int) $e->getCode(),
null,
null
);
}
$statusCode = $response->getStatusCode();
if ($statusCode < 200 || $statusCode > 299) {
throw new ApiException(
sprintf(
'[%d] Error connecting to the API (%s)',
$statusCode,
(string) $request->getUri()
),
$statusCode,
$response->getHeaders(),
(string) $response->getBody()
);
}
switch($statusCode) {
case 200:
if ('\HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations' === '\SplFileObject') {
$content = $response->getBody(); //stream goes to serializer
} else {
$content = (string) $response->getBody();
}
return [
ObjectSerializer::deserialize($content, '\HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations', []),
$response->getStatusCode(),
$response->getHeaders()
];
default:
if ('\HubSpot\Client\Crm\Deals\Model\Error' === '\SplFileObject') {
$content = $response->getBody(); //stream goes to serializer
} else {
$content = (string) $response->getBody();
}
return [
ObjectSerializer::deserialize($content, '\HubSpot\Client\Crm\Deals\Model\Error', []),
$response->getStatusCode(),
$response->getHeaders()
];
}
$returnType = '\HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations';
if ($returnType === '\SplFileObject') {
$content = $response->getBody(); //stream goes to serializer
} else {
$content = (string) $response->getBody();
}
return [
ObjectSerializer::deserialize($content, $returnType, []),
$response->getStatusCode(),
$response->getHeaders()
];
} catch (ApiException $e) {
switch ($e->getCode()) {
case 200:
$data = ObjectSerializer::deserialize(
$e->getResponseBody(),
'\HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations',
$e->getResponseHeaders()
);
$e->setResponseObject($data);
break;
default:
$data = ObjectSerializer::deserialize(
$e->getResponseBody(),
'\HubSpot\Client\Crm\Deals\Model\Error',
$e->getResponseHeaders()
);
$e->setResponseObject($data);
break;
}
throw $e;
}
}
/**
* Operation getByIdAsync
*
* Read
*
* @param string $deal_id (required)
* @param string[] $properties A comma separated list of the properties to be returned in the response. If any of the specified properties are not present on the requested object(s), they will be ignored. (optional)
* @param string[] $associations A comma separated list of object types to retrieve associated IDs for. If any of the specified associations do not exist, they will be ignored. (optional)
* @param bool $archived Whether to return only results that have been archived. (optional, default to false)
* @param string $id_property The name of a property whose values are unique for this object type (optional)
*
* @throws \InvalidArgumentException
* @return \GuzzleHttp\Promise\PromiseInterface
*/
public function getByIdAsync($deal_id, $properties = null, $associations = null, $archived = false, $id_property = null)
{
return $this->getByIdAsyncWithHttpInfo($deal_id, $properties, $associations, $archived, $id_property)
->then(
function ($response) {
return $response[0];
}
);
}
/**
* Operation getByIdAsyncWithHttpInfo
*
* Read
*
* @param string $deal_id (required)
* @param string[] $properties A comma separated list of the properties to be returned in the response. If any of the specified properties are not present on the requested object(s), they will be ignored. (optional)
* @param string[] $associations A comma separated list of object types to retrieve associated IDs for. If any of the specified associations do not exist, they will be ignored. (optional)
* @param bool $archived Whether to return only results that have been archived. (optional, default to false)
* @param string $id_property The name of a property whose values are unique for this object type (optional)
*
* @throws \InvalidArgumentException
* @return \GuzzleHttp\Promise\PromiseInterface
*/
public function getByIdAsyncWithHttpInfo($deal_id, $properties = null, $associations = null, $archived = false, $id_property = null)
{
$returnType = '\HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations';
$request = $this->getByIdRequest($deal_id, $properties, $associations, $archived, $id_property);
return $this->client
->sendAsync($request, $this->createHttpClientOption())
->then(
function ($response) use ($returnType) {
if ($returnType === '\SplFileObject') {
$content = $response->getBody(); //stream goes to serializer
} else {
$content = (string) $response->getBody();
}
return [
ObjectSerializer::deserialize($content, $returnType, []),
$response->getStatusCode(),
$response->getHeaders()
];
},
function ($exception) {
$response = $exception->getResponse();
$statusCode = $response->getStatusCode();
throw new ApiException(
sprintf(
'[%d] Error connecting to the API (%s)',
$statusCode,
$exception->getRequest()->getUri()
),
$statusCode,
$response->getHeaders(),
(string) $response->getBody()
);
}
);
}
/**
* Create request for operation 'getById'
*
* @param string $deal_id (required)
* @param string[] $properties A comma separated list of the properties to be returned in the response. If any of the specified properties are not present on the requested object(s), they will be ignored. (optional)
* @param string[] $associations A comma separated list of object types to retrieve associated IDs for. If any of the specified associations do not exist, they will be ignored. (optional)
* @param bool $archived Whether to return only results that have been archived. (optional, default to false)
* @param string $id_property The name of a property whose values are unique for this object type (optional)
*
* @throws \InvalidArgumentException
* @return \GuzzleHttp\Psr7\Request
*/
public function getByIdRequest($deal_id, $properties = null, $associations = null, $archived = false, $id_property = null)
{
// verify the required parameter 'deal_id' is set
if ($deal_id === null || (is_array($deal_id) && count($deal_id) === 0)) {
throw new \InvalidArgumentException(
'Missing the required parameter $deal_id when calling getById'
);
}
$resourcePath = '/crm/v3/objects/deals/{dealId}';
$formParams = [];
$queryParams = [];
$headerParams = [];
$httpBody = '';
$multipart = false;
// query params
if ($properties !== null) {
if('form' === 'form' && is_array($properties)) {
foreach($properties as $key => $value) {
$queryParams[$key] = $value;
}
}
else {
$queryParams['properties'] = $properties;
}
}
// query params
if ($associations !== null) {
if('form' === 'form' && is_array($associations)) {
foreach($associations as $key => $value) {
$queryParams[$key] = $value;
}
}
else {
$queryParams['associations'] = $associations;
}
}
// query params
if ($archived !== null) {
if('form' === 'form' && is_array($archived)) {
foreach($archived as $key => $value) {
$queryParams[$key] = $value;
}
}
else {
$queryParams['archived'] = $archived;
}
}
// query params
if ($id_property !== null) {
if('form' === 'form' && is_array($id_property)) {
foreach($id_property as $key => $value) {
$queryParams[$key] = $value;
}
}
else {
$queryParams['idProperty'] = $id_property;
}
}
// path params
if ($deal_id !== null) {
$resourcePath = str_replace(
'{' . 'dealId' . '}',
ObjectSerializer::toPathValue($deal_id),
$resourcePath
);
}
if ($multipart) {
$headers = $this->headerSelector->selectHeadersForMultipart(
['application/json', '*/*']
);
} else {
$headers = $this->headerSelector->selectHeaders(
['application/json', '*/*'],
[]
);
}
// for model (json/xml)
if (count($formParams) > 0) {
if ($multipart) {
$multipartContents = [];
foreach ($formParams as $formParamName => $formParamValue) {
$formParamValueItems = is_array($formParamValue) ? $formParamValue : [$formParamValue];
foreach ($formParamValueItems as $formParamValueItem) {
$multipartContents[] = [
'name' => $formParamName,
'contents' => $formParamValueItem
];
}
}
// for HTTP post (form)
$httpBody = new MultipartStream($multipartContents);
} elseif ($headers['Content-Type'] === 'application/json') {
$httpBody = \GuzzleHttp\json_encode($formParams);
} else {
// for HTTP post (form)
$httpBody = \GuzzleHttp\Psr7\Query::build($formParams);
}
}
// this endpoint requires API key authentication
$apiKey = $this->config->getApiKeyWithPrefix('hapikey');
if ($apiKey !== null) {
$queryParams['hapikey'] = $apiKey;
}
// this endpoint requires OAuth (access token)
if ($this->config->getAccessToken() !== null) {
$headers['Authorization'] = 'Bearer ' . $this->config->getAccessToken();
}
$defaultHeaders = [];
if ($this->config->getUserAgent()) {
$defaultHeaders['User-Agent'] = $this->config->getUserAgent();
}
$headers = array_merge(
$defaultHeaders,
$headerParams,
$headers
);
$query = \GuzzleHttp\Psr7\Query::build($queryParams);
return new Request(
'GET',
$this->config->getHost() . $resourcePath . ($query ? "?{$query}" : ''),
$headers,
$httpBody
);
}
/**
* Operation getPage
*
* List
*
* @param int $limit The maximum number of results to display per page. (optional, default to 10)
* @param string $after The paging cursor token of the last successfully read resource will be returned as the `paging.next.after` JSON property of a paged response containing more results. (optional)
* @param string[] $properties A comma separated list of the properties to be returned in the response. If any of the specified properties are not present on the requested object(s), they will be ignored. (optional)
* @param string[] $associations A comma separated list of object types to retrieve associated IDs for. If any of the specified associations do not exist, they will be ignored. (optional)
* @param bool $archived Whether to return only results that have been archived. (optional, default to false)
*
* @throws \HubSpot\Client\Crm\Deals\ApiException on non-2xx response
* @throws \InvalidArgumentException
* @return \HubSpot\Client\Crm\Deals\Model\CollectionResponseSimplePublicObjectWithAssociationsForwardPaging|\HubSpot\Client\Crm\Deals\Model\Error
*/
public function getPage($limit = 10, $after = null, $properties = null, $associations = null, $archived = false)
{
list($response) = $this->getPageWithHttpInfo($limit, $after, $properties, $associations, $archived);
return $response;
}
/**
* Operation getPageWithHttpInfo
*
* List
*
* @param int $limit The maximum number of results to display per page. (optional, default to 10)
* @param string $after The paging cursor token of the last successfully read resource will be returned as the `paging.next.after` JSON property of a paged response containing more results. (optional)
* @param string[] $properties A comma separated list of the properties to be returned in the response. If any of the specified properties are not present on the requested object(s), they will be ignored. (optional)
* @param string[] $associations A comma separated list of object types to retrieve associated IDs for. If any of the specified associations do not exist, they will be ignored. (optional)
* @param bool $archived Whether to return only results that have been archived. (optional, default to false)
*
* @throws \HubSpot\Client\Crm\Deals\ApiException on non-2xx response
* @throws \InvalidArgumentException
* @return array of \HubSpot\Client\Crm\Deals\Model\CollectionResponseSimplePublicObjectWithAssociationsForwardPaging|\HubSpot\Client\Crm\Deals\Model\Error, HTTP status code, HTTP response headers (array of strings)
*/
public function getPageWithHttpInfo($limit = 10, $after = null, $properties = null, $associations = null, $archived = false)
{
$request = $this->getPageRequest($limit, $after, $properties, $associations, $archived);
try {
$options = $this->createHttpClientOption();
try {
$response = $this->client->send($request, $options);
} catch (RequestException $e) {
throw new ApiException(
"[{$e->getCode()}] {$e->getMessage()}",
(int) $e->getCode(),
$e->getResponse() ? $e->getResponse()->getHeaders() : null,
$e->getResponse() ? (string) $e->getResponse()->getBody() : null
);
} catch (ConnectException $e) {
throw new ApiException(
"[{$e->getCode()}] {$e->getMessage()}",
(int) $e->getCode(),
null,
null
);
}
$statusCode = $response->getStatusCode();
if ($statusCode < 200 || $statusCode > 299) {
throw new ApiException(
sprintf(
'[%d] Error connecting to the API (%s)',
$statusCode,
(string) $request->getUri()
),
$statusCode,
$response->getHeaders(),
(string) $response->getBody()
);
}
switch($statusCode) {
case 200:
if ('\HubSpot\Client\Crm\Deals\Model\CollectionResponseSimplePublicObjectWithAssociationsForwardPaging' === '\SplFileObject') {
$content = $response->getBody(); //stream goes to serializer
} else {
$content = (string) $response->getBody();
}
return [
ObjectSerializer::deserialize($content, '\HubSpot\Client\Crm\Deals\Model\CollectionResponseSimplePublicObjectWithAssociationsForwardPaging', []),
$response->getStatusCode(),
$response->getHeaders()
];
default:
if ('\HubSpot\Client\Crm\Deals\Model\Error' === '\SplFileObject') {
$content = $response->getBody(); //stream goes to serializer
} else {
$content = (string) $response->getBody();
}
return [
ObjectSerializer::deserialize($content, '\HubSpot\Client\Crm\Deals\Model\Error', []),
$response->getStatusCode(),
$response->getHeaders()
];
}
$returnType = '\HubSpot\Client\Crm\Deals\Model\CollectionResponseSimplePublicObjectWithAssociationsForwardPaging';
if ($returnType === '\SplFileObject') {
$content = $response->getBody(); //stream goes to serializer
} else {
$content = (string) $response->getBody();
}
return [
ObjectSerializer::deserialize($content, $returnType, []),
$response->getStatusCode(),
$response->getHeaders()
];
} catch (ApiException $e) {
switch ($e->getCode()) {
case 200:
$data = ObjectSerializer::deserialize(
$e->getResponseBody(),
'\HubSpot\Client\Crm\Deals\Model\CollectionResponseSimplePublicObjectWithAssociationsForwardPaging',
$e->getResponseHeaders()
);
$e->setResponseObject($data);
break;
default:
$data = ObjectSerializer::deserialize(
$e->getResponseBody(),
'\HubSpot\Client\Crm\Deals\Model\Error',
$e->getResponseHeaders()
);
$e->setResponseObject($data);
break;
}
throw $e;
}
}
/**
* Operation getPageAsync
*
* List
*
* @param int $limit The maximum number of results to display per page. (optional, default to 10)
* @param string $after The paging cursor token of the last successfully read resource will be returned as the `paging.next.after` JSON property of a paged response containing more results. (optional)
* @param string[] $properties A comma separated list of the properties to be returned in the response. If any of the specified properties are not present on the requested object(s), they will be ignored. (optional)
* @param string[] $associations A comma separated list of object types to retrieve associated IDs for. If any of the specified associations do not exist, they will be ignored. (optional)
* @param bool $archived Whether to return only results that have been archived. (optional, default to false)
*
* @throws \InvalidArgumentException
* @return \GuzzleHttp\Promise\PromiseInterface
*/
public function getPageAsync($limit = 10, $after = null, $properties = null, $associations = null, $archived = false)
{
return $this->getPageAsyncWithHttpInfo($limit, $after, $properties, $associations, $archived)
->then(
function ($response) {
return $response[0];
}
);
}
/**
* Operation getPageAsyncWithHttpInfo
*
* List
*
* @param int $limit The maximum number of results to display per page. (optional, default to 10)
* @param string $after The paging cursor token of the last successfully read resource will be returned as the `paging.next.after` JSON property of a paged response containing more results. (optional)
* @param string[] $properties A comma separated list of the properties to be returned in the response. If any of the specified properties are not present on the requested object(s), they will be ignored. (optional)
* @param string[] $associations A comma separated list of object types to retrieve associated IDs for. If any of the specified associations do not exist, they will be ignored. (optional)
* @param bool $archived Whether to return only results that have been archived. (optional, default to false)
*
* @throws \InvalidArgumentException
* @return \GuzzleHttp\Promise\PromiseInterface
*/
public function getPageAsyncWithHttpInfo($limit = 10, $after = null, $properties = null, $associations = null, $archived = false)
{
$returnType = '\HubSpot\Client\Crm\Deals\Model\CollectionResponseSimplePublicObjectWithAssociationsForwardPaging';
$request = $this->getPageRequest($limit, $after, $properties, $associations, $archived);
return $this->client
->sendAsync($request, $this->createHttpClientOption())
->then(
function ($response) use ($returnType) {
if ($returnType === '\SplFileObject') {
$content = $response->getBody(); //stream goes to serializer
} else {
$content = (string) $response->getBody();
}
return [
ObjectSerializer::deserialize($content, $returnType, []),
$response->getStatusCode(),
$response->getHeaders()
];
},
function ($exception) {
$response = $exception->getResponse();
$statusCode = $response->getStatusCode();
throw new ApiException(
sprintf(
'[%d] Error connecting to the API (%s)',
$statusCode,
$exception->getRequest()->getUri()
),
$statusCode,
$response->getHeaders(),
(string) $response->getBody()
);
}
);
}
/**
* Create request for operation 'getPage'
*
* @param int $limit The maximum number of results to display per page. (optional, default to 10)
* @param string $after The paging cursor token of the last successfully read resource will be returned as the `paging.next.after` JSON property of a paged response containing more results. (optional)
* @param string[] $properties A comma separated list of the properties to be returned in the response. If any of the specified properties are not present on the requested object(s), they will be ignored. (optional)
* @param string[] $associations A comma separated list of object types to retrieve associated IDs for. If any of the specified associations do not exist, they will be ignored. (optional)
* @param bool $archived Whether to return only results that have been archived. (optional, default to false)
*
* @throws \InvalidArgumentException
* @return \GuzzleHttp\Psr7\Request
*/
public function getPageRequest($limit = 10, $after = null, $properties = null, $associations = null, $archived = false)
{
$resourcePath = '...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.034242023,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: master","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"Editor for custom.log","depth":4,"bounds":{"left":0.5475399,"top":0.0726257,"width":0.44082448,"height":0.9066241},"on_screen":true,"role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Reader Mode","depth":4,"bounds":{"left":0.47207448,"top":0.17318435,"width":0.034574468,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n/**\n * BasicApi\n * PHP version 7.3\n *\n * @category Class\n * @package HubSpot\\Client\\Crm\\Deals\n * @author OpenAPI Generator team\n * @link https://openapi-generator.tech\n */\n\n/**\n * Deals\n *\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: v3\n * Generated by: https://openapi-generator.tech\n * OpenAPI Generator version: 5.3.0\n */\n\n/**\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\nnamespace HubSpot\\Client\\Crm\\Deals\\Api;\n\nuse GuzzleHttp\\Client;\nuse GuzzleHttp\\ClientInterface;\nuse GuzzleHttp\\Exception\\RequestException;\nuse GuzzleHttp\\Exception\\ConnectException;\nuse GuzzleHttp\\Psr7\\MultipartStream;\nuse GuzzleHttp\\Psr7\\Request;\nuse GuzzleHttp\\RequestOptions;\nuse HubSpot\\Client\\Crm\\Deals\\ApiException;\nuse HubSpot\\Client\\Crm\\Deals\\Configuration;\nuse HubSpot\\Client\\Crm\\Deals\\HeaderSelector;\nuse HubSpot\\Client\\Crm\\Deals\\ObjectSerializer;\n\n/**\n * BasicApi Class Doc Comment\n *\n * @category Class\n * @package HubSpot\\Client\\Crm\\Deals\n * @author OpenAPI Generator team\n * @link https://openapi-generator.tech\n */\nclass BasicApi\n{\n /**\n * @var ClientInterface\n */\n protected $client;\n\n /**\n * @var Configuration\n */\n protected $config;\n\n /**\n * @var HeaderSelector\n */\n protected $headerSelector;\n\n /**\n * @var int Host index\n */\n protected $hostIndex;\n\n /**\n * @param ClientInterface $client\n * @param Configuration $config\n * @param HeaderSelector $selector\n * @param int $hostIndex (Optional) host index to select the list of hosts if defined in the OpenAPI spec\n */\n public function __construct(\n ClientInterface $client = null,\n Configuration $config = null,\n HeaderSelector $selector = null,\n $hostIndex = 0\n ) {\n $this->client = $client ?: new Client();\n $this->config = $config ?: new Configuration();\n $this->headerSelector = $selector ?: new HeaderSelector();\n $this->hostIndex = $hostIndex;\n }\n\n /**\n * Set the host index\n *\n * @param int $hostIndex Host index (required)\n */\n public function setHostIndex($hostIndex): void\n {\n $this->hostIndex = $hostIndex;\n }\n\n /**\n * Get the host index\n *\n * @return int Host index\n */\n public function getHostIndex()\n {\n return $this->hostIndex;\n }\n\n /**\n * @return Configuration\n */\n public function getConfig()\n {\n return $this->config;\n }\n\n /**\n * Operation archive\n *\n * Archive\n *\n * @param string $deal_id deal_id (required)\n *\n * @throws \\HubSpot\\Client\\Crm\\Deals\\ApiException on non-2xx response\n * @throws \\InvalidArgumentException\n * @return void\n */\n public function archive($deal_id)\n {\n $this->archiveWithHttpInfo($deal_id);\n }\n\n /**\n * Operation archiveWithHttpInfo\n *\n * Archive\n *\n * @param string $deal_id (required)\n *\n * @throws \\HubSpot\\Client\\Crm\\Deals\\ApiException on non-2xx response\n * @throws \\InvalidArgumentException\n * @return array of null, HTTP status code, HTTP response headers (array of strings)\n */\n public function archiveWithHttpInfo($deal_id)\n {\n $request = $this->archiveRequest($deal_id);\n\n try {\n $options = $this->createHttpClientOption();\n try {\n $response = $this->client->send($request, $options);\n } catch (RequestException $e) {\n throw new ApiException(\n \"[{$e->getCode()}] {$e->getMessage()}\",\n (int) $e->getCode(),\n $e->getResponse() ? $e->getResponse()->getHeaders() : null,\n $e->getResponse() ? (string) $e->getResponse()->getBody() : null\n );\n } catch (ConnectException $e) {\n throw new ApiException(\n \"[{$e->getCode()}] {$e->getMessage()}\",\n (int) $e->getCode(),\n null,\n null\n );\n }\n\n $statusCode = $response->getStatusCode();\n\n if ($statusCode < 200 || $statusCode > 299) {\n throw new ApiException(\n sprintf(\n '[%d] Error connecting to the API (%s)',\n $statusCode,\n (string) $request->getUri()\n ),\n $statusCode,\n $response->getHeaders(),\n (string) $response->getBody()\n );\n }\n\n return [null, $statusCode, $response->getHeaders()];\n\n } catch (ApiException $e) {\n switch ($e->getCode()) {\n default:\n $data = ObjectSerializer::deserialize(\n $e->getResponseBody(),\n '\\HubSpot\\Client\\Crm\\Deals\\Model\\Error',\n $e->getResponseHeaders()\n );\n $e->setResponseObject($data);\n break;\n }\n throw $e;\n }\n }\n\n /**\n * Operation archiveAsync\n *\n * Archive\n *\n * @param string $deal_id (required)\n *\n * @throws \\InvalidArgumentException\n * @return \\GuzzleHttp\\Promise\\PromiseInterface\n */\n public function archiveAsync($deal_id)\n {\n return $this->archiveAsyncWithHttpInfo($deal_id)\n ->then(\n function ($response) {\n return $response[0];\n }\n );\n }\n\n /**\n * Operation archiveAsyncWithHttpInfo\n *\n * Archive\n *\n * @param string $deal_id (required)\n *\n * @throws \\InvalidArgumentException\n * @return \\GuzzleHttp\\Promise\\PromiseInterface\n */\n public function archiveAsyncWithHttpInfo($deal_id)\n {\n $returnType = '';\n $request = $this->archiveRequest($deal_id);\n\n return $this->client\n ->sendAsync($request, $this->createHttpClientOption())\n ->then(\n function ($response) use ($returnType) {\n return [null, $response->getStatusCode(), $response->getHeaders()];\n },\n function ($exception) {\n $response = $exception->getResponse();\n $statusCode = $response->getStatusCode();\n throw new ApiException(\n sprintf(\n '[%d] Error connecting to the API (%s)',\n $statusCode,\n $exception->getRequest()->getUri()\n ),\n $statusCode,\n $response->getHeaders(),\n (string) $response->getBody()\n );\n }\n );\n }\n\n /**\n * Create request for operation 'archive'\n *\n * @param string $deal_id (required)\n *\n * @throws \\InvalidArgumentException\n * @return \\GuzzleHttp\\Psr7\\Request\n */\n public function archiveRequest($deal_id)\n {\n // verify the required parameter 'deal_id' is set\n if ($deal_id === null || (is_array($deal_id) && count($deal_id) === 0)) {\n throw new \\InvalidArgumentException(\n 'Missing the required parameter $deal_id when calling archive'\n );\n }\n\n $resourcePath = '/crm/v3/objects/deals/{dealId}';\n $formParams = [];\n $queryParams = [];\n $headerParams = [];\n $httpBody = '';\n $multipart = false;\n\n\n\n // path params\n if ($deal_id !== null) {\n $resourcePath = str_replace(\n '{' . 'dealId' . '}',\n ObjectSerializer::toPathValue($deal_id),\n $resourcePath\n );\n }\n\n\n if ($multipart) {\n $headers = $this->headerSelector->selectHeadersForMultipart(\n ['*/*']\n );\n } else {\n $headers = $this->headerSelector->selectHeaders(\n ['*/*'],\n []\n );\n }\n\n // for model (json/xml)\n if (count($formParams) > 0) {\n if ($multipart) {\n $multipartContents = [];\n foreach ($formParams as $formParamName => $formParamValue) {\n $formParamValueItems = is_array($formParamValue) ? $formParamValue : [$formParamValue];\n foreach ($formParamValueItems as $formParamValueItem) {\n $multipartContents[] = [\n 'name' => $formParamName,\n 'contents' => $formParamValueItem\n ];\n }\n }\n // for HTTP post (form)\n $httpBody = new MultipartStream($multipartContents);\n\n } elseif ($headers['Content-Type'] === 'application/json') {\n $httpBody = \\GuzzleHttp\\json_encode($formParams);\n\n } else {\n // for HTTP post (form)\n $httpBody = \\GuzzleHttp\\Psr7\\Query::build($formParams);\n }\n }\n\n // this endpoint requires API key authentication\n $apiKey = $this->config->getApiKeyWithPrefix('hapikey');\n if ($apiKey !== null) {\n $queryParams['hapikey'] = $apiKey;\n }\n // this endpoint requires OAuth (access token)\n if ($this->config->getAccessToken() !== null) {\n $headers['Authorization'] = 'Bearer ' . $this->config->getAccessToken();\n }\n\n $defaultHeaders = [];\n if ($this->config->getUserAgent()) {\n $defaultHeaders['User-Agent'] = $this->config->getUserAgent();\n }\n\n $headers = array_merge(\n $defaultHeaders,\n $headerParams,\n $headers\n );\n\n $query = \\GuzzleHttp\\Psr7\\Query::build($queryParams);\n return new Request(\n 'DELETE',\n $this->config->getHost() . $resourcePath . ($query ? \"?{$query}\" : ''),\n $headers,\n $httpBody\n );\n }\n\n /**\n * Operation create\n *\n * Create\n *\n * @param \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectInput $simple_public_object_input simple_public_object_input (required)\n *\n * @throws \\HubSpot\\Client\\Crm\\Deals\\ApiException on non-2xx response\n * @throws \\InvalidArgumentException\n * @return \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObject|\\HubSpot\\Client\\Crm\\Deals\\Model\\Error\n */\n public function create($simple_public_object_input)\n {\n list($response) = $this->createWithHttpInfo($simple_public_object_input);\n return $response;\n }\n\n /**\n * Operation createWithHttpInfo\n *\n * Create\n *\n * @param \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectInput $simple_public_object_input (required)\n *\n * @throws \\HubSpot\\Client\\Crm\\Deals\\ApiException on non-2xx response\n * @throws \\InvalidArgumentException\n * @return array of \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObject|\\HubSpot\\Client\\Crm\\Deals\\Model\\Error, HTTP status code, HTTP response headers (array of strings)\n */\n public function createWithHttpInfo($simple_public_object_input)\n {\n $request = $this->createRequest($simple_public_object_input);\n\n try {\n $options = $this->createHttpClientOption();\n try {\n $response = $this->client->send($request, $options);\n } catch (RequestException $e) {\n throw new ApiException(\n \"[{$e->getCode()}] {$e->getMessage()}\",\n (int) $e->getCode(),\n $e->getResponse() ? $e->getResponse()->getHeaders() : null,\n $e->getResponse() ? (string) $e->getResponse()->getBody() : null\n );\n } catch (ConnectException $e) {\n throw new ApiException(\n \"[{$e->getCode()}] {$e->getMessage()}\",\n (int) $e->getCode(),\n null,\n null\n );\n }\n\n $statusCode = $response->getStatusCode();\n\n if ($statusCode < 200 || $statusCode > 299) {\n throw new ApiException(\n sprintf(\n '[%d] Error connecting to the API (%s)',\n $statusCode,\n (string) $request->getUri()\n ),\n $statusCode,\n $response->getHeaders(),\n (string) $response->getBody()\n );\n }\n\n switch($statusCode) {\n case 201:\n if ('\\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObject' === '\\SplFileObject') {\n $content = $response->getBody(); //stream goes to serializer\n } else {\n $content = (string) $response->getBody();\n }\n\n return [\n ObjectSerializer::deserialize($content, '\\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObject', []),\n $response->getStatusCode(),\n $response->getHeaders()\n ];\n default:\n if ('\\HubSpot\\Client\\Crm\\Deals\\Model\\Error' === '\\SplFileObject') {\n $content = $response->getBody(); //stream goes to serializer\n } else {\n $content = (string) $response->getBody();\n }\n\n return [\n ObjectSerializer::deserialize($content, '\\HubSpot\\Client\\Crm\\Deals\\Model\\Error', []),\n $response->getStatusCode(),\n $response->getHeaders()\n ];\n }\n\n $returnType = '\\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObject';\n if ($returnType === '\\SplFileObject') {\n $content = $response->getBody(); //stream goes to serializer\n } else {\n $content = (string) $response->getBody();\n }\n\n return [\n ObjectSerializer::deserialize($content, $returnType, []),\n $response->getStatusCode(),\n $response->getHeaders()\n ];\n\n } catch (ApiException $e) {\n switch ($e->getCode()) {\n case 201:\n $data = ObjectSerializer::deserialize(\n $e->getResponseBody(),\n '\\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObject',\n $e->getResponseHeaders()\n );\n $e->setResponseObject($data);\n break;\n default:\n $data = ObjectSerializer::deserialize(\n $e->getResponseBody(),\n '\\HubSpot\\Client\\Crm\\Deals\\Model\\Error',\n $e->getResponseHeaders()\n );\n $e->setResponseObject($data);\n break;\n }\n throw $e;\n }\n }\n\n /**\n * Operation createAsync\n *\n * Create\n *\n * @param \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectInput $simple_public_object_input (required)\n *\n * @throws \\InvalidArgumentException\n * @return \\GuzzleHttp\\Promise\\PromiseInterface\n */\n public function createAsync($simple_public_object_input)\n {\n return $this->createAsyncWithHttpInfo($simple_public_object_input)\n ->then(\n function ($response) {\n return $response[0];\n }\n );\n }\n\n /**\n * Operation createAsyncWithHttpInfo\n *\n * Create\n *\n * @param \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectInput $simple_public_object_input (required)\n *\n * @throws \\InvalidArgumentException\n * @return \\GuzzleHttp\\Promise\\PromiseInterface\n */\n public function createAsyncWithHttpInfo($simple_public_object_input)\n {\n $returnType = '\\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObject';\n $request = $this->createRequest($simple_public_object_input);\n\n return $this->client\n ->sendAsync($request, $this->createHttpClientOption())\n ->then(\n function ($response) use ($returnType) {\n if ($returnType === '\\SplFileObject') {\n $content = $response->getBody(); //stream goes to serializer\n } else {\n $content = (string) $response->getBody();\n }\n\n return [\n ObjectSerializer::deserialize($content, $returnType, []),\n $response->getStatusCode(),\n $response->getHeaders()\n ];\n },\n function ($exception) {\n $response = $exception->getResponse();\n $statusCode = $response->getStatusCode();\n throw new ApiException(\n sprintf(\n '[%d] Error connecting to the API (%s)',\n $statusCode,\n $exception->getRequest()->getUri()\n ),\n $statusCode,\n $response->getHeaders(),\n (string) $response->getBody()\n );\n }\n );\n }\n\n /**\n * Create request for operation 'create'\n *\n * @param \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectInput $simple_public_object_input (required)\n *\n * @throws \\InvalidArgumentException\n * @return \\GuzzleHttp\\Psr7\\Request\n */\n public function createRequest($simple_public_object_input)\n {\n // verify the required parameter 'simple_public_object_input' is set\n if ($simple_public_object_input === null || (is_array($simple_public_object_input) && count($simple_public_object_input) === 0)) {\n throw new \\InvalidArgumentException(\n 'Missing the required parameter $simple_public_object_input when calling create'\n );\n }\n\n $resourcePath = '/crm/v3/objects/deals';\n $formParams = [];\n $queryParams = [];\n $headerParams = [];\n $httpBody = '';\n $multipart = false;\n\n\n\n\n\n if ($multipart) {\n $headers = $this->headerSelector->selectHeadersForMultipart(\n ['application/json', '*/*']\n );\n } else {\n $headers = $this->headerSelector->selectHeaders(\n ['application/json', '*/*'],\n ['application/json']\n );\n }\n\n // for model (json/xml)\n if (isset($simple_public_object_input)) {\n if ($headers['Content-Type'] === 'application/json') {\n $httpBody = \\GuzzleHttp\\json_encode(ObjectSerializer::sanitizeForSerialization($simple_public_object_input));\n } else {\n $httpBody = $simple_public_object_input;\n }\n } elseif (count($formParams) > 0) {\n if ($multipart) {\n $multipartContents = [];\n foreach ($formParams as $formParamName => $formParamValue) {\n $formParamValueItems = is_array($formParamValue) ? $formParamValue : [$formParamValue];\n foreach ($formParamValueItems as $formParamValueItem) {\n $multipartContents[] = [\n 'name' => $formParamName,\n 'contents' => $formParamValueItem\n ];\n }\n }\n // for HTTP post (form)\n $httpBody = new MultipartStream($multipartContents);\n\n } elseif ($headers['Content-Type'] === 'application/json') {\n $httpBody = \\GuzzleHttp\\json_encode($formParams);\n\n } else {\n // for HTTP post (form)\n $httpBody = \\GuzzleHttp\\Psr7\\Query::build($formParams);\n }\n }\n\n // this endpoint requires API key authentication\n $apiKey = $this->config->getApiKeyWithPrefix('hapikey');\n if ($apiKey !== null) {\n $queryParams['hapikey'] = $apiKey;\n }\n // this endpoint requires OAuth (access token)\n if ($this->config->getAccessToken() !== null) {\n $headers['Authorization'] = 'Bearer ' . $this->config->getAccessToken();\n }\n\n $defaultHeaders = [];\n if ($this->config->getUserAgent()) {\n $defaultHeaders['User-Agent'] = $this->config->getUserAgent();\n }\n\n $headers = array_merge(\n $defaultHeaders,\n $headerParams,\n $headers\n );\n\n $query = \\GuzzleHttp\\Psr7\\Query::build($queryParams);\n return new Request(\n 'POST',\n $this->config->getHost() . $resourcePath . ($query ? \"?{$query}\" : ''),\n $headers,\n $httpBody\n );\n }\n\n /**\n * Operation getById\n *\n * Read\n *\n * @param string $deal_id deal_id (required)\n * @param string[] $properties A comma separated list of the properties to be returned in the response. If any of the specified properties are not present on the requested object(s), they will be ignored. (optional)\n * @param string[] $associations A comma separated list of object types to retrieve associated IDs for. If any of the specified associations do not exist, they will be ignored. (optional)\n * @param bool $archived Whether to return only results that have been archived. (optional, default to false)\n * @param string $id_property The name of a property whose values are unique for this object type (optional)\n *\n * @throws \\HubSpot\\Client\\Crm\\Deals\\ApiException on non-2xx response\n * @throws \\InvalidArgumentException\n * @return \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations|\\HubSpot\\Client\\Crm\\Deals\\Model\\Error\n */\n public function getById($deal_id, $properties = null, $associations = null, $archived = false, $id_property = null)\n {\n list($response) = $this->getByIdWithHttpInfo($deal_id, $properties, $associations, $archived, $id_property);\n return $response;\n }\n\n /**\n * Operation getByIdWithHttpInfo\n *\n * Read\n *\n * @param string $deal_id (required)\n * @param string[] $properties A comma separated list of the properties to be returned in the response. If any of the specified properties are not present on the requested object(s), they will be ignored. (optional)\n * @param string[] $associations A comma separated list of object types to retrieve associated IDs for. If any of the specified associations do not exist, they will be ignored. (optional)\n * @param bool $archived Whether to return only results that have been archived. (optional, default to false)\n * @param string $id_property The name of a property whose values are unique for this object type (optional)\n *\n * @throws \\HubSpot\\Client\\Crm\\Deals\\ApiException on non-2xx response\n * @throws \\InvalidArgumentException\n * @return array of \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations|\\HubSpot\\Client\\Crm\\Deals\\Model\\Error, HTTP status code, HTTP response headers (array of strings)\n */\n public function getByIdWithHttpInfo($deal_id, $properties = null, $associations = null, $archived = false, $id_property = null)\n {\n $request = $this->getByIdRequest($deal_id, $properties, $associations, $archived, $id_property);\n\n try {\n $options = $this->createHttpClientOption();\n try {\n $response = $this->client->send($request, $options);\n } catch (RequestException $e) {\n throw new ApiException(\n \"[{$e->getCode()}] {$e->getMessage()}\",\n (int) $e->getCode(),\n $e->getResponse() ? $e->getResponse()->getHeaders() : null,\n $e->getResponse() ? (string) $e->getResponse()->getBody() : null\n );\n } catch (ConnectException $e) {\n throw new ApiException(\n \"[{$e->getCode()}] {$e->getMessage()}\",\n (int) $e->getCode(),\n null,\n null\n );\n }\n\n $statusCode = $response->getStatusCode();\n\n if ($statusCode < 200 || $statusCode > 299) {\n throw new ApiException(\n sprintf(\n '[%d] Error connecting to the API (%s)',\n $statusCode,\n (string) $request->getUri()\n ),\n $statusCode,\n $response->getHeaders(),\n (string) $response->getBody()\n );\n }\n\n switch($statusCode) {\n case 200:\n if ('\\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations' === '\\SplFileObject') {\n $content = $response->getBody(); //stream goes to serializer\n } else {\n $content = (string) $response->getBody();\n }\n\n return [\n ObjectSerializer::deserialize($content, '\\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations', []),\n $response->getStatusCode(),\n $response->getHeaders()\n ];\n default:\n if ('\\HubSpot\\Client\\Crm\\Deals\\Model\\Error' === '\\SplFileObject') {\n $content = $response->getBody(); //stream goes to serializer\n } else {\n $content = (string) $response->getBody();\n }\n\n return [\n ObjectSerializer::deserialize($content, '\\HubSpot\\Client\\Crm\\Deals\\Model\\Error', []),\n $response->getStatusCode(),\n $response->getHeaders()\n ];\n }\n\n $returnType = '\\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations';\n if ($returnType === '\\SplFileObject') {\n $content = $response->getBody(); //stream goes to serializer\n } else {\n $content = (string) $response->getBody();\n }\n\n return [\n ObjectSerializer::deserialize($content, $returnType, []),\n $response->getStatusCode(),\n $response->getHeaders()\n ];\n\n } catch (ApiException $e) {\n switch ($e->getCode()) {\n case 200:\n $data = ObjectSerializer::deserialize(\n $e->getResponseBody(),\n '\\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations',\n $e->getResponseHeaders()\n );\n $e->setResponseObject($data);\n break;\n default:\n $data = ObjectSerializer::deserialize(\n $e->getResponseBody(),\n '\\HubSpot\\Client\\Crm\\Deals\\Model\\Error',\n $e->getResponseHeaders()\n );\n $e->setResponseObject($data);\n break;\n }\n throw $e;\n }\n }\n\n /**\n * Operation getByIdAsync\n *\n * Read\n *\n * @param string $deal_id (required)\n * @param string[] $properties A comma separated list of the properties to be returned in the response. If any of the specified properties are not present on the requested object(s), they will be ignored. (optional)\n * @param string[] $associations A comma separated list of object types to retrieve associated IDs for. If any of the specified associations do not exist, they will be ignored. (optional)\n * @param bool $archived Whether to return only results that have been archived. (optional, default to false)\n * @param string $id_property The name of a property whose values are unique for this object type (optional)\n *\n * @throws \\InvalidArgumentException\n * @return \\GuzzleHttp\\Promise\\PromiseInterface\n */\n public function getByIdAsync($deal_id, $properties = null, $associations = null, $archived = false, $id_property = null)\n {\n return $this->getByIdAsyncWithHttpInfo($deal_id, $properties, $associations, $archived, $id_property)\n ->then(\n function ($response) {\n return $response[0];\n }\n );\n }\n\n /**\n * Operation getByIdAsyncWithHttpInfo\n *\n * Read\n *\n * @param string $deal_id (required)\n * @param string[] $properties A comma separated list of the properties to be returned in the response. If any of the specified properties are not present on the requested object(s), they will be ignored. (optional)\n * @param string[] $associations A comma separated list of object types to retrieve associated IDs for. If any of the specified associations do not exist, they will be ignored. (optional)\n * @param bool $archived Whether to return only results that have been archived. (optional, default to false)\n * @param string $id_property The name of a property whose values are unique for this object type (optional)\n *\n * @throws \\InvalidArgumentException\n * @return \\GuzzleHttp\\Promise\\PromiseInterface\n */\n public function getByIdAsyncWithHttpInfo($deal_id, $properties = null, $associations = null, $archived = false, $id_property = null)\n {\n $returnType = '\\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations';\n $request = $this->getByIdRequest($deal_id, $properties, $associations, $archived, $id_property);\n\n return $this->client\n ->sendAsync($request, $this->createHttpClientOption())\n ->then(\n function ($response) use ($returnType) {\n if ($returnType === '\\SplFileObject') {\n $content = $response->getBody(); //stream goes to serializer\n } else {\n $content = (string) $response->getBody();\n }\n\n return [\n ObjectSerializer::deserialize($content, $returnType, []),\n $response->getStatusCode(),\n $response->getHeaders()\n ];\n },\n function ($exception) {\n $response = $exception->getResponse();\n $statusCode = $response->getStatusCode();\n throw new ApiException(\n sprintf(\n '[%d] Error connecting to the API (%s)',\n $statusCode,\n $exception->getRequest()->getUri()\n ),\n $statusCode,\n $response->getHeaders(),\n (string) $response->getBody()\n );\n }\n );\n }\n\n /**\n * Create request for operation 'getById'\n *\n * @param string $deal_id (required)\n * @param string[] $properties A comma separated list of the properties to be returned in the response. If any of the specified properties are not present on the requested object(s), they will be ignored. (optional)\n * @param string[] $associations A comma separated list of object types to retrieve associated IDs for. If any of the specified associations do not exist, they will be ignored. (optional)\n * @param bool $archived Whether to return only results that have been archived. (optional, default to false)\n * @param string $id_property The name of a property whose values are unique for this object type (optional)\n *\n * @throws \\InvalidArgumentException\n * @return \\GuzzleHttp\\Psr7\\Request\n */\n public function getByIdRequest($deal_id, $properties = null, $associations = null, $archived = false, $id_property = null)\n {\n // verify the required parameter 'deal_id' is set\n if ($deal_id === null || (is_array($deal_id) && count($deal_id) === 0)) {\n throw new \\InvalidArgumentException(\n 'Missing the required parameter $deal_id when calling getById'\n );\n }\n\n $resourcePath = '/crm/v3/objects/deals/{dealId}';\n $formParams = [];\n $queryParams = [];\n $headerParams = [];\n $httpBody = '';\n $multipart = false;\n\n // query params\n if ($properties !== null) {\n if('form' === 'form' && is_array($properties)) {\n foreach($properties as $key => $value) {\n $queryParams[$key] = $value;\n }\n }\n else {\n $queryParams['properties'] = $properties;\n }\n }\n // query params\n if ($associations !== null) {\n if('form' === 'form' && is_array($associations)) {\n foreach($associations as $key => $value) {\n $queryParams[$key] = $value;\n }\n }\n else {\n $queryParams['associations'] = $associations;\n }\n }\n // query params\n if ($archived !== null) {\n if('form' === 'form' && is_array($archived)) {\n foreach($archived as $key => $value) {\n $queryParams[$key] = $value;\n }\n }\n else {\n $queryParams['archived'] = $archived;\n }\n }\n // query params\n if ($id_property !== null) {\n if('form' === 'form' && is_array($id_property)) {\n foreach($id_property as $key => $value) {\n $queryParams[$key] = $value;\n }\n }\n else {\n $queryParams['idProperty'] = $id_property;\n }\n }\n\n\n // path params\n if ($deal_id !== null) {\n $resourcePath = str_replace(\n '{' . 'dealId' . '}',\n ObjectSerializer::toPathValue($deal_id),\n $resourcePath\n );\n }\n\n\n if ($multipart) {\n $headers = $this->headerSelector->selectHeadersForMultipart(\n ['application/json', '*/*']\n );\n } else {\n $headers = $this->headerSelector->selectHeaders(\n ['application/json', '*/*'],\n []\n );\n }\n\n // for model (json/xml)\n if (count($formParams) > 0) {\n if ($multipart) {\n $multipartContents = [];\n foreach ($formParams as $formParamName => $formParamValue) {\n $formParamValueItems = is_array($formParamValue) ? $formParamValue : [$formParamValue];\n foreach ($formParamValueItems as $formParamValueItem) {\n $multipartContents[] = [\n 'name' => $formParamName,\n 'contents' => $formParamValueItem\n ];\n }\n }\n // for HTTP post (form)\n $httpBody = new MultipartStream($multipartContents);\n\n } elseif ($headers['Content-Type'] === 'application/json') {\n $httpBody = \\GuzzleHttp\\json_encode($formParams);\n\n } else {\n // for HTTP post (form)\n $httpBody = \\GuzzleHttp\\Psr7\\Query::build($formParams);\n }\n }\n\n // this endpoint requires API key authentication\n $apiKey = $this->config->getApiKeyWithPrefix('hapikey');\n if ($apiKey !== null) {\n $queryParams['hapikey'] = $apiKey;\n }\n // this endpoint requires OAuth (access token)\n if ($this->config->getAccessToken() !== null) {\n $headers['Authorization'] = 'Bearer ' . $this->config->getAccessToken();\n }\n\n $defaultHeaders = [];\n if ($this->config->getUserAgent()) {\n $defaultHeaders['User-Agent'] = $this->config->getUserAgent();\n }\n\n $headers = array_merge(\n $defaultHeaders,\n $headerParams,\n $headers\n );\n\n $query = \\GuzzleHttp\\Psr7\\Query::build($queryParams);\n return new Request(\n 'GET',\n $this->config->getHost() . $resourcePath . ($query ? \"?{$query}\" : ''),\n $headers,\n $httpBody\n );\n }\n\n /**\n * Operation getPage\n *\n * List\n *\n * @param int $limit The maximum number of results to display per page. (optional, default to 10)\n * @param string $after The paging cursor token of the last successfully read resource will be returned as the `paging.next.after` JSON property of a paged response containing more results. (optional)\n * @param string[] $properties A comma separated list of the properties to be returned in the response. If any of the specified properties are not present on the requested object(s), they will be ignored. (optional)\n * @param string[] $associations A comma separated list of object types to retrieve associated IDs for. If any of the specified associations do not exist, they will be ignored. (optional)\n * @param bool $archived Whether to return only results that have been archived. (optional, default to false)\n *\n * @throws \\HubSpot\\Client\\Crm\\Deals\\ApiException on non-2xx response\n * @throws \\InvalidArgumentException\n * @return \\HubSpot\\Client\\Crm\\Deals\\Model\\CollectionResponseSimplePublicObjectWithAssociationsForwardPaging|\\HubSpot\\Client\\Crm\\Deals\\Model\\Error\n */\n public function getPage($limit = 10, $after = null, $properties = null, $associations = null, $archived = false)\n {\n list($response) = $this->getPageWithHttpInfo($limit, $after, $properties, $associations, $archived);\n return $response;\n }\n\n /**\n * Operation getPageWithHttpInfo\n *\n * List\n *\n * @param int $limit The maximum number of results to display per page. (optional, default to 10)\n * @param string $after The paging cursor token of the last successfully read resource will be returned as the `paging.next.after` JSON property of a paged response containing more results. (optional)\n * @param string[] $properties A comma separated list of the properties to be returned in the response. If any of the specified properties are not present on the requested object(s), they will be ignored. (optional)\n * @param string[] $associations A comma separated list of object types to retrieve associated IDs for. If any of the specified associations do not exist, they will be ignored. (optional)\n * @param bool $archived Whether to return only results that have been archived. (optional, default to false)\n *\n * @throws \\HubSpot\\Client\\Crm\\Deals\\ApiException on non-2xx response\n * @throws \\InvalidArgumentException\n * @return array of \\HubSpot\\Client\\Crm\\Deals\\Model\\CollectionResponseSimplePublicObjectWithAssociationsForwardPaging|\\HubSpot\\Client\\Crm\\Deals\\Model\\Error, HTTP status code, HTTP response headers (array of strings)\n */\n public function getPageWithHttpInfo($limit = 10, $after = null, $properties = null, $associations = null, $archived = false)\n {\n $request = $this->getPageRequest($limit, $after, $properties, $associations, $archived);\n\n try {\n $options = $this->createHttpClientOption();\n try {\n $response = $this->client->send($request, $options);\n } catch (RequestException $e) {\n throw new ApiException(\n \"[{$e->getCode()}] {$e->getMessage()}\",\n (int) $e->getCode(),\n $e->getResponse() ? $e->getResponse()->getHeaders() : null,\n $e->getResponse() ? (string) $e->getResponse()->getBody() : null\n );\n } catch (ConnectException $e) {\n throw new ApiException(\n \"[{$e->getCode()}] {$e->getMessage()}\",\n (int) $e->getCode(),\n null,\n null\n );\n }\n\n $statusCode = $response->getStatusCode();\n\n if ($statusCode < 200 || $statusCode > 299) {\n throw new ApiException(\n sprintf(\n '[%d] Error connecting to the API (%s)',\n $statusCode,\n (string) $request->getUri()\n ),\n $statusCode,\n $response->getHeaders(),\n (string) $response->getBody()\n );\n }\n\n switch($statusCode) {\n case 200:\n if ('\\HubSpot\\Client\\Crm\\Deals\\Model\\CollectionResponseSimplePublicObjectWithAssociationsForwardPaging' === '\\SplFileObject') {\n $content = $response->getBody(); //stream goes to serializer\n } else {\n $content = (string) $response->getBody();\n }\n\n return [\n ObjectSerializer::deserialize($content, '\\HubSpot\\Client\\Crm\\Deals\\Model\\CollectionResponseSimplePublicObjectWithAssociationsForwardPaging', []),\n $response->getStatusCode(),\n $response->getHeaders()\n ];\n default:\n if ('\\HubSpot\\Client\\Crm\\Deals\\Model\\Error' === '\\SplFileObject') {\n $content = $response->getBody(); //stream goes to serializer\n } else {\n $content = (string) $response->getBody();\n }\n\n return [\n ObjectSerializer::deserialize($content, '\\HubSpot\\Client\\Crm\\Deals\\Model\\Error', []),\n $response->getStatusCode(),\n $response->getHeaders()\n ];\n }\n\n $returnType = '\\HubSpot\\Client\\Crm\\Deals\\Model\\CollectionResponseSimplePublicObjectWithAssociationsForwardPaging';\n if ($returnType === '\\SplFileObject') {\n $content = $response->getBody(); //stream goes to serializer\n } else {\n $content = (string) $response->getBody();\n }\n\n return [\n ObjectSerializer::deserialize($content, $returnType, []),\n $response->getStatusCode(),\n $response->getHeaders()\n ];\n\n } catch (ApiException $e) {\n switch ($e->getCode()) {\n case 200:\n $data = ObjectSerializer::deserialize(\n $e->getResponseBody(),\n '\\HubSpot\\Client\\Crm\\Deals\\Model\\CollectionResponseSimplePublicObjectWithAssociationsForwardPaging',\n $e->getResponseHeaders()\n );\n $e->setResponseObject($data);\n break;\n default:\n $data = ObjectSerializer::deserialize(\n $e->getResponseBody(),\n '\\HubSpot\\Client\\Crm\\Deals\\Model\\Error',\n $e->getResponseHeaders()\n );\n $e->setResponseObject($data);\n break;\n }\n throw $e;\n }\n }\n\n /**\n * Operation getPageAsync\n *\n * List\n *\n * @param int $limit The maximum number of results to display per page. (optional, default to 10)\n * @param string $after The paging cursor token of the last successfully read resource will be returned as the `paging.next.after` JSON property of a paged response containing more results. (optional)\n * @param string[] $properties A comma separated list of the properties to be returned in the response. If any of the specified properties are not present on the requested object(s), they will be ignored. (optional)\n * @param string[] $associations A comma separated list of object types to retrieve associated IDs for. If any of the specified associations do not exist, they will be ignored. (optional)\n * @param bool $archived Whether to return only results that have been archived. (optional, default to false)\n *\n * @throws \\InvalidArgumentException\n * @return \\GuzzleHttp\\Promise\\PromiseInterface\n */\n public function getPageAsync($limit = 10, $after = null, $properties = null, $associations = null, $archived = false)\n {\n return $this->getPageAsyncWithHttpInfo($limit, $after, $properties, $associations, $archived)\n ->then(\n function ($response) {\n return $response[0];\n }\n );\n }\n\n /**\n * Operation getPageAsyncWithHttpInfo\n *\n * List\n *\n * @param int $limit The maximum number of results to display per page. (optional, default to 10)\n * @param string $after The paging cursor token of the last successfully read resource will be returned as the `paging.next.after` JSON property of a paged response containing more results. (optional)\n * @param string[] $properties A comma separated list of the properties to be returned in the response. If any of the specified properties are not present on the requested object(s), they will be ignored. (optional)\n * @param string[] $associations A comma separated list of object types to retrieve associated IDs for. If any of the specified associations do not exist, they will be ignored. (optional)\n * @param bool $archived Whether to return only results that have been archived. (optional, default to false)\n *\n * @throws \\InvalidArgumentException\n * @return \\GuzzleHttp\\Promise\\PromiseInterface\n */\n public function getPageAsyncWithHttpInfo($limit = 10, $after = null, $properties = null, $associations = null, $archived = false)\n {\n $returnType = '\\HubSpot\\Client\\Crm\\Deals\\Model\\CollectionResponseSimplePublicObjectWithAssociationsForwardPaging';\n $request = $this->getPageRequest($limit, $after, $properties, $associations, $archived);\n\n return $this->client\n ->sendAsync($request, $this->createHttpClientOption())\n ->then(\n function ($response) use ($returnType) {\n if ($returnType === '\\SplFileObject') {\n $content = $response->getBody(); //stream goes to serializer\n } else {\n $content = (string) $response->getBody();\n }\n\n return [\n ObjectSerializer::deserialize($content, $returnType, []),\n $response->getStatusCode(),\n $response->getHeaders()\n ];\n },\n function ($exception) {\n $response = $exception->getResponse();\n $statusCode = $response->getStatusCode();\n throw new ApiException(\n sprintf(\n '[%d] Error connecting to the API (%s)',\n $statusCode,\n $exception->getRequest()->getUri()\n ),\n $statusCode,\n $response->getHeaders(),\n (string) $response->getBody()\n );\n }\n );\n }\n\n /**\n * Create request for operation 'getPage'\n *\n * @param int $limit The maximum number of results to display per page. (optional, default to 10)\n * @param string $after The paging cursor token of the last successfully read resource will be returned as the `paging.next.after` JSON property of a paged response containing more results. (optional)\n * @param string[] $properties A comma separated list of the properties to be returned in the response. If any of the specified properties are not present on the requested object(s), they will be ignored. (optional)\n * @param string[] $associations A comma separated list of object types to retrieve associated IDs for. If any of the specified associations do not exist, they will be ignored. (optional)\n * @param bool $archived Whether to return only results that have been archived. (optional, default to false)\n *\n * @throws \\InvalidArgumentException\n * @return \\GuzzleHttp\\Psr7\\Request\n */\n public function getPageRequest($limit = 10, $after = null, $properties = null, $associations = null, $archived = false)\n {\n\n $resourcePath = '/crm/v3/objects/deals';\n $formParams = [];\n $queryParams = [];\n $headerParams = [];\n $httpBody = '';\n $multipart = false;\n\n // query params\n if ($limit !== null) {\n if('form' === 'form' && is_array($limit)) {\n foreach($limit as $key => $value) {\n $queryParams[$key] = $value;\n }\n }\n else {\n $queryParams['limit'] = $limit;\n }\n }\n // query params\n if ($after !== null) {\n if('form' === 'form' && is_array($after)) {\n foreach($after as $key => $value) {\n $queryParams[$key] = $value;\n }\n }\n else {\n $queryParams['after'] = $after;\n }\n }\n // query params\n if ($properties !== null) {\n if('form' === 'form' && is_array($properties)) {\n foreach($properties as $key => $value) {\n $queryParams[$key] = $value;\n }\n }\n else {\n $queryParams['properties'] = $properties;\n }\n }\n // query params\n if ($associations !== null) {\n if('form' === 'form' && is_array($associations)) {\n foreach($associations as $key => $value) {\n $queryParams[$key] = $value;\n }\n }\n else {\n $queryParams['associations'] = $associations;\n }\n }\n // query params\n if ($archived !== null) {\n if('form' === 'form' && is_array($archived)) {\n foreach($archived as $key => $value) {\n $queryParams[$key] = $value;\n }\n }\n else {\n $queryParams['archived'] = $archived;\n }\n }\n\n\n\n\n if ($multipart) {\n $headers = $this->headerSelector->selectHeadersForMultipart(\n ['application/json', '*/*']\n );\n } else {\n $headers = $this->headerSelector->selectHeaders(\n ['application/json', '*/*'],\n []\n );\n }\n\n // for model (json/xml)\n if (count($formParams) > 0) {\n if ($multipart) {\n $multipartContents = [];\n foreach ($formParams as $formParamName => $formParamValue) {\n $formParamValueItems = is_array($formParamValue) ? $formParamValue : [$formParamValue];\n foreach ($formParamValueItems as $formParamValueItem) {\n $multipartContents[] = [\n 'name' => $formParamName,\n 'contents' => $formParamValueItem\n ];\n }\n }\n // for HTTP post (form)\n $httpBody = new MultipartStream($multipartContents);\n\n } elseif ($headers['Content-Type'] === 'application/json') {\n $httpBody = \\GuzzleHttp\\json_encode($formParams);\n\n } else {\n // for HTTP post (form)\n $httpBody = \\GuzzleHttp\\Psr7\\Query::build($formParams);\n }\n }\n\n // this endpoint requires API key authentication\n $apiKey = $this->config->getApiKeyWithPrefix('hapikey');\n if ($apiKey !== null) {\n $queryParams['hapikey'] = $apiKey;\n }\n // this endpoint requires OAuth (access token)\n if ($this->config->getAccessToken() !== null) {\n $headers['Authorization'] = 'Bearer ' . $this->config->getAccessToken();\n }\n\n $defaultHeaders = [];\n if ($this->config->getUserAgent()) {\n $defaultHeaders['User-Agent'] = $this->config->getUserAgent();\n }\n\n $headers = array_merge(\n $defaultHeaders,\n $headerParams,\n $headers\n );\n\n $query = \\GuzzleHttp\\Psr7\\Query::build($queryParams);\n return new Request(\n 'GET',\n $this->config->getHost() . $resourcePath . ($query ? \"?{$query}\" : ''),\n $headers,\n $httpBody\n );\n }\n\n /**\n * Operation update\n *\n * Update\n *\n * @param string $deal_id deal_id (required)\n * @param \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectInput $simple_public_object_input simple_public_object_input (required)\n * @param string $id_property The name of a property whose values are unique for this object type (optional)\n *\n * @throws \\HubSpot\\Client\\Crm\\Deals\\ApiException on non-2xx response\n * @throws \\InvalidArgumentException\n * @return \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObject|\\HubSpot\\Client\\Crm\\Deals\\Model\\Error\n */\n public function update($deal_id, $simple_public_object_input, $id_property = null)\n {\n list($response) = $this->updateWithHttpInfo($deal_id, $simple_public_object_input, $id_property);\n return $response;\n }\n\n /**\n * Operation updateWithHttpInfo\n *\n * Update\n *\n * @param string $deal_id (required)\n * @param \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectInput $simple_public_object_input (required)\n * @param string $id_property The name of a property whose values are unique for this object type (optional)\n *\n * @throws \\HubSpot\\Client\\Crm\\Deals\\ApiException on non-2xx response\n * @throws \\InvalidArgumentException\n * @return array of \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObject|\\HubSpot\\Client\\Crm\\Deals\\Model\\Error, HTTP status code, HTTP response headers (array of strings)\n */\n public function updateWithHttpInfo($deal_id, $simple_public_object_input, $id_property = null)\n {\n $request = $this->updateRequest($deal_id, $simple_public_object_input, $id_property);\n\n try {\n $options = $this->createHttpClientOption();\n try {\n $response = $this->client->send($request, $options);\n } catch (RequestException $e) {\n throw new ApiException(\n \"[{$e->getCode()}] {$e->getMessage()}\",\n (int) $e->getCode(),\n $e->getResponse() ? $e->getResponse()->getHeaders() : null,\n $e->getResponse() ? (string) $e->getResponse()->getBody() : null\n );\n } catch (ConnectException $e) {\n throw new ApiException(\n \"[{$e->getCode()}] {$e->getMessage()}\",\n (int) $e->getCode(),\n null,\n null\n );\n }\n\n $statusCode = $response->getStatusCode();\n\n if ($statusCode < 200 || $statusCode > 299) {\n throw new ApiException(\n sprintf(\n '[%d] Error connecting to the API (%s)',\n $statusCode,\n (string) $request->getUri()\n ),\n $statusCode,\n $response->getHeaders(),\n (string) $response->getBody()\n );\n }\n\n switch($statusCode) {\n case 200:\n if ('\\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObject' === '\\SplFileObject') {\n $content = $response->getBody(); //stream goes to serializer\n } else {\n $content = (string) $response->getBody();\n }\n\n return [\n ObjectSerializer::deserialize($content, '\\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObject', []),\n $response->getStatusCode(),\n $response->getHeaders()\n ];\n default:\n if ('\\HubSpot\\Client\\Crm\\Deals\\Model\\Error' === '\\SplFileObject') {\n $content = $response->getBody(); //stream goes to serializer\n } else {\n $content = (string) $response->getBody();\n }\n\n return [\n ObjectSerializer::deserialize($content, '\\HubSpot\\Client\\Crm\\Deals\\Model\\Error', []),\n $response->getStatusCode(),\n $response->getHeaders()\n ];\n }\n\n $returnType = '\\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObject';\n if ($returnType === '\\SplFileObject') {\n $content = $response->getBody(); //stream goes to serializer\n } else {\n $content = (string) $response->getBody();\n }\n\n return [\n ObjectSerializer::deserialize($content, $returnType, []),\n $response->getStatusCode(),\n $response->getHeaders()\n ];\n\n } catch (ApiException $e) {\n switch ($e->getCode()) {\n case 200:\n $data = ObjectSerializer::deserialize(\n $e->getResponseBody(),\n '\\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObject',\n $e->getResponseHeaders()\n );\n $e->setResponseObject($data);\n break;\n default:\n $data = ObjectSerializer::deserialize(\n $e->getResponseBody(),\n '\\HubSpot\\Client\\Crm\\Deals\\Model\\Error',\n $e->getResponseHeaders()\n );\n $e->setResponseObject($data);\n break;\n }\n throw $e;\n }\n }\n\n /**\n * Operation updateAsync\n *\n * Update\n *\n * @param string $deal_id (required)\n * @param \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectInput $simple_public_object_input (required)\n * @param string $id_property The name of a property whose values are unique for this object type (optional)\n *\n * @throws \\InvalidArgumentException\n * @return \\GuzzleHttp\\Promise\\PromiseInterface\n */\n public function updateAsync($deal_id, $simple_public_object_input, $id_property = null)\n {\n return $this->updateAsyncWithHttpInfo($deal_id, $simple_public_object_input, $id_property)\n ->then(\n function ($response) {\n return $response[0];\n }\n );\n }\n\n /**\n * Operation updateAsyncWithHttpInfo\n *\n * Update\n *\n * @param string $deal_id (required)\n * @param \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectInput $simple_public_object_input (required)\n * @param string $id_property The name of a property whose values are unique for this object type (optional)\n *\n * @throws \\InvalidArgumentException\n * @return \\GuzzleHttp\\Promise\\PromiseInterface\n */\n public function updateAsyncWithHttpInfo($deal_id, $simple_public_object_input, $id_property = null)\n {\n $returnType = '\\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObject';\n $request = $this->updateRequest($deal_id, $simple_public_object_input, $id_property);\n\n return $this->client\n ->sendAsync($request, $this->createHttpClientOption())\n ->then(\n function ($response) use ($returnType) {\n if ($returnType === '\\SplFileObject') {\n $content = $response->getBody(); //stream goes to serializer\n } else {\n $content = (string) $response->getBody();\n }\n\n return [\n ObjectSerializer::deserialize($content, $returnType, []),\n $response->getStatusCode(),\n $response->getHeaders()\n ];\n },\n function ($exception) {\n $response = $exception->getResponse();\n $statusCode = $response->getStatusCode();\n throw new ApiException(\n sprintf(\n '[%d] Error connecting to the API (%s)',\n $statusCode,\n $exception->getRequest()->getUri()\n ),\n $statusCode,\n $response->getHeaders(),\n (string) $response->getBody()\n );\n }\n );\n }\n\n /**\n * Create request for operation 'update'\n *\n * @param string $deal_id (required)\n * @param \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectInput $simple_public_object_input (required)\n * @param string $id_property The name of a property whose values are unique for this object type (optional)\n *\n * @throws \\InvalidArgumentException\n * @return \\GuzzleHttp\\Psr7\\Request\n */\n public function updateRequest($deal_id, $simple_public_object_input, $id_property = null)\n {\n // verify the required parameter 'deal_id' is set\n if ($deal_id === null || (is_array($deal_id) && count($deal_id) === 0)) {\n throw new \\InvalidArgumentException(\n 'Missing the required parameter $deal_id when calling update'\n );\n }\n // verify the required parameter 'simple_public_object_input' is set\n if ($simple_public_object_input === null || (is_array($simple_public_object_input) && count($simple_public_object_input) === 0)) {\n throw new \\InvalidArgumentException(\n 'Missing the required parameter $simple_public_object_input when calling update'\n );\n }\n\n $resourcePath = '/crm/v3/objects/deals/{dealId}';\n $formParams = [];\n $queryParams = [];\n $headerParams = [];\n $httpBody = '';\n $multipart = false;\n\n // query params\n if ($id_property !== null) {\n if('form' === 'form' && is_array($id_property)) {\n foreach($id_property as $key => $value) {\n $queryParams[$key] = $value;\n }\n }\n else {\n $queryParams['idProperty'] = $id_property;\n }\n }\n\n\n // path params\n if ($deal_id !== null) {\n $resourcePath = str_replace(\n '{' . 'dealId' . '}',\n ObjectSerializer::toPathValue($deal_id),\n $resourcePath\n );\n }\n\n\n if ($multipart) {\n $headers = $this->headerSelector->selectHeadersForMultipart(\n ['application/json', '*/*']\n );\n } else {\n $headers = $this->headerSelector->selectHeaders(\n ['application/json', '*/*'],\n ['application/json']\n );\n }\n\n // for model (json/xml)\n if (isset($simple_public_object_input)) {\n if ($headers['Content-Type'] === 'application/json') {\n $httpBody = \\GuzzleHttp\\json_encode(ObjectSerializer::sanitizeForSerialization($simple_public_object_input));\n } else {\n $httpBody = $simple_public_object_input;\n }\n } elseif (count($formParams) > 0) {\n if ($multipart) {\n $multipartContents = [];\n foreach ($formParams as $formParamName => $formParamValue) {\n $formParamValueItems = is_array($formParamValue) ? $formParamValue : [$formParamValue];\n foreach ($formParamValueItems as $formParamValueItem) {\n $multipartContents[] = [\n 'name' => $formParamName,\n 'contents' => $formParamValueItem\n ];\n }\n }\n // for HTTP post (form)\n $httpBody = new MultipartStream($multipartContents);\n\n } elseif ($headers['Content-Type'] === 'application/json') {\n $httpBody = \\GuzzleHttp\\json_encode($formParams);\n\n } else {\n // for HTTP post (form)\n $httpBody = \\GuzzleHttp\\Psr7\\Query::build($formParams);\n }\n }\n\n // this endpoint requires API key authentication\n $apiKey = $this->config->getApiKeyWithPrefix('hapikey');\n if ($apiKey !== null) {\n $queryParams['hapikey'] = $apiKey;\n }\n // this endpoint requires OAuth (access token)\n if ($this->config->getAccessToken() !== null) {\n $headers['Authorization'] = 'Bearer ' . $this->config->getAccessToken();\n }\n\n $defaultHeaders = [];\n if ($this->config->getUserAgent()) {\n $defaultHeaders['User-Agent'] = $this->config->getUserAgent();\n }\n\n $headers = array_merge(\n $defaultHeaders,\n $headerParams,\n $headers\n );\n\n $query = \\GuzzleHttp\\Psr7\\Query::build($queryParams);\n return new Request(\n 'PATCH',\n $this->config->getHost() . $resourcePath . ($query ? \"?{$query}\" : ''),\n $headers,\n $httpBody\n );\n }\n\n /**\n * Create http client option\n *\n * @throws \\RuntimeException on file opening failure\n * @return array of http client options\n */\n protected function createHttpClientOption()\n {\n $options = [];\n if ($this->config->getDebug()) {\n $options[RequestOptions::DEBUG] = fopen($this->config->getDebugFile(), 'a');\n if (!$options[RequestOptions::DEBUG]) {\n throw new \\RuntimeException('Failed to open the debug file: ' . $this->config->getDebugFile());\n }\n }\n\n return $options;\n }\n}","depth":4,"on_screen":true,"value":"<?php\n/**\n * BasicApi\n * PHP version 7.3\n *\n * @category Class\n * @package HubSpot\\Client\\Crm\\Deals\n * @author OpenAPI Generator team\n * @link https://openapi-generator.tech\n */\n\n/**\n * Deals\n *\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: v3\n * Generated by: https://openapi-generator.tech\n * OpenAPI Generator version: 5.3.0\n */\n\n/**\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\nnamespace HubSpot\\Client\\Crm\\Deals\\Api;\n\nuse GuzzleHttp\\Client;\nuse GuzzleHttp\\ClientInterface;\nuse GuzzleHttp\\Exception\\RequestException;\nuse GuzzleHttp\\Exception\\ConnectException;\nuse GuzzleHttp\\Psr7\\MultipartStream;\nuse GuzzleHttp\\Psr7\\Request;\nuse GuzzleHttp\\RequestOptions;\nuse HubSpot\\Client\\Crm\\Deals\\ApiException;\nuse HubSpot\\Client\\Crm\\Deals\\Configuration;\nuse HubSpot\\Client\\Crm\\Deals\\HeaderSelector;\nuse HubSpot\\Client\\Crm\\Deals\\ObjectSerializer;\n\n/**\n * BasicApi Class Doc Comment\n *\n * @category Class\n * @package HubSpot\\Client\\Crm\\Deals\n * @author OpenAPI Generator team\n * @link https://openapi-generator.tech\n */\nclass BasicApi\n{\n /**\n * @var ClientInterface\n */\n protected $client;\n\n /**\n * @var Configuration\n */\n protected $config;\n\n /**\n * @var HeaderSelector\n */\n protected $headerSelector;\n\n /**\n * @var int Host index\n */\n protected $hostIndex;\n\n /**\n * @param ClientInterface $client\n * @param Configuration $config\n * @param HeaderSelector $selector\n * @param int $hostIndex (Optional) host index to select the list of hosts if defined in the OpenAPI spec\n */\n public function __construct(\n ClientInterface $client = null,\n Configuration $config = null,\n HeaderSelector $selector = null,\n $hostIndex = 0\n ) {\n $this->client = $client ?: new Client();\n $this->config = $config ?: new Configuration();\n $this->headerSelector = $selector ?: new HeaderSelector();\n $this->hostIndex = $hostIndex;\n }\n\n /**\n * Set the host index\n *\n * @param int $hostIndex Host index (required)\n */\n public function setHostIndex($hostIndex): void\n {\n $this->hostIndex = $hostIndex;\n }\n\n /**\n * Get the host index\n *\n * @return int Host index\n */\n public function getHostIndex()\n {\n return $this->hostIndex;\n }\n\n /**\n * @return Configuration\n */\n public function getConfig()\n {\n return $this->config;\n }\n\n /**\n * Operation archive\n *\n * Archive\n *\n * @param string $deal_id deal_id (required)\n *\n * @throws \\HubSpot\\Client\\Crm\\Deals\\ApiException on non-2xx response\n * @throws \\InvalidArgumentException\n * @return void\n */\n public function archive($deal_id)\n {\n $this->archiveWithHttpInfo($deal_id);\n }\n\n /**\n * Operation archiveWithHttpInfo\n *\n * Archive\n *\n * @param string $deal_id (required)\n *\n * @throws \\HubSpot\\Client\\Crm\\Deals\\ApiException on non-2xx response\n * @throws \\InvalidArgumentException\n * @return array of null, HTTP status code, HTTP response headers (array of strings)\n */\n public function archiveWithHttpInfo($deal_id)\n {\n $request = $this->archiveRequest($deal_id);\n\n try {\n $options = $this->createHttpClientOption();\n try {\n $response = $this->client->send($request, $options);\n } catch (RequestException $e) {\n throw new ApiException(\n \"[{$e->getCode()}] {$e->getMessage()}\",\n (int) $e->getCode(),\n $e->getResponse() ? $e->getResponse()->getHeaders() : null,\n $e->getResponse() ? (string) $e->getResponse()->getBody() : null\n );\n } catch (ConnectException $e) {\n throw new ApiException(\n \"[{$e->getCode()}] {$e->getMessage()}\",\n (int) $e->getCode(),\n null,\n null\n );\n }\n\n $statusCode = $response->getStatusCode();\n\n if ($statusCode < 200 || $statusCode > 299) {\n throw new ApiException(\n sprintf(\n '[%d] Error connecting to the API (%s)',\n $statusCode,\n (string) $request->getUri()\n ),\n $statusCode,\n $response->getHeaders(),\n (string) $response->getBody()\n );\n }\n\n return [null, $statusCode, $response->getHeaders()];\n\n } catch (ApiException $e) {\n switch ($e->getCode()) {\n default:\n $data = ObjectSerializer::deserialize(\n $e->getResponseBody(),\n '\\HubSpot\\Client\\Crm\\Deals\\Model\\Error',\n $e->getResponseHeaders()\n );\n $e->setResponseObject($data);\n break;\n }\n throw $e;\n }\n }\n\n /**\n * Operation archiveAsync\n *\n * Archive\n *\n * @param string $deal_id (required)\n *\n * @throws \\InvalidArgumentException\n * @return \\GuzzleHttp\\Promise\\PromiseInterface\n */\n public function archiveAsync($deal_id)\n {\n return $this->archiveAsyncWithHttpInfo($deal_id)\n ->then(\n function ($response) {\n return $response[0];\n }\n );\n }\n\n /**\n * Operation archiveAsyncWithHttpInfo\n *\n * Archive\n *\n * @param string $deal_id (required)\n *\n * @throws \\InvalidArgumentException\n * @return \\GuzzleHttp\\Promise\\PromiseInterface\n */\n public function archiveAsyncWithHttpInfo($deal_id)\n {\n $returnType = '';\n $request = $this->archiveRequest($deal_id);\n\n return $this->client\n ->sendAsync($request, $this->createHttpClientOption())\n ->then(\n function ($response) use ($returnType) {\n return [null, $response->getStatusCode(), $response->getHeaders()];\n },\n function ($exception) {\n $response = $exception->getResponse();\n $statusCode = $response->getStatusCode();\n throw new ApiException(\n sprintf(\n '[%d] Error connecting to the API (%s)',\n $statusCode,\n $exception->getRequest()->getUri()\n ),\n $statusCode,\n $response->getHeaders(),\n (string) $response->getBody()\n );\n }\n );\n }\n\n /**\n * Create request for operation 'archive'\n *\n * @param string $deal_id (required)\n *\n * @throws \\InvalidArgumentException\n * @return \\GuzzleHttp\\Psr7\\Request\n */\n public function archiveRequest($deal_id)\n {\n // verify the required parameter 'deal_id' is set\n if ($deal_id === null || (is_array($deal_id) && count($deal_id) === 0)) {\n throw new \\InvalidArgumentException(\n 'Missing the required parameter $deal_id when calling archive'\n );\n }\n\n $resourcePath = '/crm/v3/objects/deals/{dealId}';\n $formParams = [];\n $queryParams = [];\n $headerParams = [];\n $httpBody = '';\n $multipart = false;\n\n\n\n // path params\n if ($deal_id !== null) {\n $resourcePath = str_replace(\n '{' . 'dealId' . '}',\n ObjectSerializer::toPathValue($deal_id),\n $resourcePath\n );\n }\n\n\n if ($multipart) {\n $headers = $this->headerSelector->selectHeadersForMultipart(\n ['*/*']\n );\n } else {\n $headers = $this->headerSelector->selectHeaders(\n ['*/*'],\n []\n );\n }\n\n // for model (json/xml)\n if (count($formParams) > 0) {\n if ($multipart) {\n $multipartContents = [];\n foreach ($formParams as $formParamName => $formParamValue) {\n $formParamValueItems = is_array($formParamValue) ? $formParamValue : [$formParamValue];\n foreach ($formParamValueItems as $formParamValueItem) {\n $multipartContents[] = [\n 'name' => $formParamName,\n 'contents' => $formParamValueItem\n ];\n }\n }\n // for HTTP post (form)\n $httpBody = new MultipartStream($multipartContents);\n\n } elseif ($headers['Content-Type'] === 'application/json') {\n $httpBody = \\GuzzleHttp\\json_encode($formParams);\n\n } else {\n // for HTTP post (form)\n $httpBody = \\GuzzleHttp\\Psr7\\Query::build($formParams);\n }\n }\n\n // this endpoint requires API key authentication\n $apiKey = $this->config->getApiKeyWithPrefix('hapikey');\n if ($apiKey !== null) {\n $queryParams['hapikey'] = $apiKey;\n }\n // this endpoint requires OAuth (access token)\n if ($this->config->getAccessToken() !== null) {\n $headers['Authorization'] = 'Bearer ' . $this->config->getAccessToken();\n }\n\n $defaultHeaders = [];\n if ($this->config->getUserAgent()) {\n $defaultHeaders['User-Agent'] = $this->config->getUserAgent();\n }\n\n $headers = array_merge(\n $defaultHeaders,\n $headerParams,\n $headers\n );\n\n $query = \\GuzzleHttp\\Psr7\\Query::build($queryParams);\n return new Request(\n 'DELETE',\n $this->config->getHost() . $resourcePath . ($query ? \"?{$query}\" : ''),\n $headers,\n $httpBody\n );\n }\n\n /**\n * Operation create\n *\n * Create\n *\n * @param \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectInput $simple_public_object_input simple_public_object_input (required)\n *\n * @throws \\HubSpot\\Client\\Crm\\Deals\\ApiException on non-2xx response\n * @throws \\InvalidArgumentException\n * @return \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObject|\\HubSpot\\Client\\Crm\\Deals\\Model\\Error\n */\n public function create($simple_public_object_input)\n {\n list($response) = $this->createWithHttpInfo($simple_public_object_input);\n return $response;\n }\n\n /**\n * Operation createWithHttpInfo\n *\n * Create\n *\n * @param \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectInput $simple_public_object_input (required)\n *\n * @throws \\HubSpot\\Client\\Crm\\Deals\\ApiException on non-2xx response\n * @throws \\InvalidArgumentException\n * @return array of \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObject|\\HubSpot\\Client\\Crm\\Deals\\Model\\Error, HTTP status code, HTTP response headers (array of strings)\n */\n public function createWithHttpInfo($simple_public_object_input)\n {\n $request = $this->createRequest($simple_public_object_input);\n\n try {\n $options = $this->createHttpClientOption();\n try {\n $response = $this->client->send($request, $options);\n } catch (RequestException $e) {\n throw new ApiException(\n \"[{$e->getCode()}] {$e->getMessage()}\",\n (int) $e->getCode(),\n $e->getResponse() ? $e->getResponse()->getHeaders() : null,\n $e->getResponse() ? (string) $e->getResponse()->getBody() : null\n );\n } catch (ConnectException $e) {\n throw new ApiException(\n \"[{$e->getCode()}] {$e->getMessage()}\",\n (int) $e->getCode(),\n null,\n null\n );\n }\n\n $statusCode = $response->getStatusCode();\n\n if ($statusCode < 200 || $statusCode > 299) {\n throw new ApiException(\n sprintf(\n '[%d] Error connecting to the API (%s)',\n $statusCode,\n (string) $request->getUri()\n ),\n $statusCode,\n $response->getHeaders(),\n (string) $response->getBody()\n );\n }\n\n switch($statusCode) {\n case 201:\n if ('\\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObject' === '\\SplFileObject') {\n $content = $response->getBody(); //stream goes to serializer\n } else {\n $content = (string) $response->getBody();\n }\n\n return [\n ObjectSerializer::deserialize($content, '\\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObject', []),\n $response->getStatusCode(),\n $response->getHeaders()\n ];\n default:\n if ('\\HubSpot\\Client\\Crm\\Deals\\Model\\Error' === '\\SplFileObject') {\n $content = $response->getBody(); //stream goes to serializer\n } else {\n $content = (string) $response->getBody();\n }\n\n return [\n ObjectSerializer::deserialize($content, '\\HubSpot\\Client\\Crm\\Deals\\Model\\Error', []),\n $response->getStatusCode(),\n $response->getHeaders()\n ];\n }\n\n $returnType = '\\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObject';\n if ($returnType === '\\SplFileObject') {\n $content = $response->getBody(); //stream goes to serializer\n } else {\n $content = (string) $response->getBody();\n }\n\n return [\n ObjectSerializer::deserialize($content, $returnType, []),\n $response->getStatusCode(),\n $response->getHeaders()\n ];\n\n } catch (ApiException $e) {\n switch ($e->getCode()) {\n case 201:\n $data = ObjectSerializer::deserialize(\n $e->getResponseBody(),\n '\\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObject',\n $e->getResponseHeaders()\n );\n $e->setResponseObject($data);\n break;\n default:\n $data = ObjectSerializer::deserialize(\n $e->getResponseBody(),\n '\\HubSpot\\Client\\Crm\\Deals\\Model\\Error',\n $e->getResponseHeaders()\n );\n $e->setResponseObject($data);\n break;\n }\n throw $e;\n }\n }\n\n /**\n * Operation createAsync\n *\n * Create\n *\n * @param \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectInput $simple_public_object_input (required)\n *\n * @throws \\InvalidArgumentException\n * @return \\GuzzleHttp\\Promise\\PromiseInterface\n */\n public function createAsync($simple_public_object_input)\n {\n return $this->createAsyncWithHttpInfo($simple_public_object_input)\n ->then(\n function ($response) {\n return $response[0];\n }\n );\n }\n\n /**\n * Operation createAsyncWithHttpInfo\n *\n * Create\n *\n * @param \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectInput $simple_public_object_input (required)\n *\n * @throws \\InvalidArgumentException\n * @return \\GuzzleHttp\\Promise\\PromiseInterface\n */\n public function createAsyncWithHttpInfo($simple_public_object_input)\n {\n $returnType = '\\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObject';\n $request = $this->createRequest($simple_public_object_input);\n\n return $this->client\n ->sendAsync($request, $this->createHttpClientOption())\n ->then(\n function ($response) use ($returnType) {\n if ($returnType === '\\SplFileObject') {\n $content = $response->getBody(); //stream goes to serializer\n } else {\n $content = (string) $response->getBody();\n }\n\n return [\n ObjectSerializer::deserialize($content, $returnType, []),\n $response->getStatusCode(),\n $response->getHeaders()\n ];\n },\n function ($exception) {\n $response = $exception->getResponse();\n $statusCode = $response->getStatusCode();\n throw new ApiException(\n sprintf(\n '[%d] Error connecting to the API (%s)',\n $statusCode,\n $exception->getRequest()->getUri()\n ),\n $statusCode,\n $response->getHeaders(),\n (string) $response->getBody()\n );\n }\n );\n }\n\n /**\n * Create request for operation 'create'\n *\n * @param \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectInput $simple_public_object_input (required)\n *\n * @throws \\InvalidArgumentException\n * @return \\GuzzleHttp\\Psr7\\Request\n */\n public function createRequest($simple_public_object_input)\n {\n // verify the required parameter 'simple_public_object_input' is set\n if ($simple_public_object_input === null || (is_array($simple_public_object_input) && count($simple_public_object_input) === 0)) {\n throw new \\InvalidArgumentException(\n 'Missing the required parameter $simple_public_object_input when calling create'\n );\n }\n\n $resourcePath = '/crm/v3/objects/deals';\n $formParams = [];\n $queryParams = [];\n $headerParams = [];\n $httpBody = '';\n $multipart = false;\n\n\n\n\n\n if ($multipart) {\n $headers = $this->headerSelector->selectHeadersForMultipart(\n ['application/json', '*/*']\n );\n } else {\n $headers = $this->headerSelector->selectHeaders(\n ['application/json', '*/*'],\n ['application/json']\n );\n }\n\n // for model (json/xml)\n if (isset($simple_public_object_input)) {\n if ($headers['Content-Type'] === 'application/json') {\n $httpBody = \\GuzzleHttp\\json_encode(ObjectSerializer::sanitizeForSerialization($simple_public_object_input));\n } else {\n $httpBody = $simple_public_object_input;\n }\n } elseif (count($formParams) > 0) {\n if ($multipart) {\n $multipartContents = [];\n foreach ($formParams as $formParamName => $formParamValue) {\n $formParamValueItems = is_array($formParamValue) ? $formParamValue : [$formParamValue];\n foreach ($formParamValueItems as $formParamValueItem) {\n $multipartContents[] = [\n 'name' => $formParamName,\n 'contents' => $formParamValueItem\n ];\n }\n }\n // for HTTP post (form)\n $httpBody = new MultipartStream($multipartContents);\n\n } elseif ($headers['Content-Type'] === 'application/json') {\n $httpBody = \\GuzzleHttp\\json_encode($formParams);\n\n } else {\n // for HTTP post (form)\n $httpBody = \\GuzzleHttp\\Psr7\\Query::build($formParams);\n }\n }\n\n // this endpoint requires API key authentication\n $apiKey = $this->config->getApiKeyWithPrefix('hapikey');\n if ($apiKey !== null) {\n $queryParams['hapikey'] = $apiKey;\n }\n // this endpoint requires OAuth (access token)\n if ($this->config->getAccessToken() !== null) {\n $headers['Authorization'] = 'Bearer ' . $this->config->getAccessToken();\n }\n\n $defaultHeaders = [];\n if ($this->config->getUserAgent()) {\n $defaultHeaders['User-Agent'] = $this->config->getUserAgent();\n }\n\n $headers = array_merge(\n $defaultHeaders,\n $headerParams,\n $headers\n );\n\n $query = \\GuzzleHttp\\Psr7\\Query::build($queryParams);\n return new Request(\n 'POST',\n $this->config->getHost() . $resourcePath . ($query ? \"?{$query}\" : ''),\n $headers,\n $httpBody\n );\n }\n\n /**\n * Operation getById\n *\n * Read\n *\n * @param string $deal_id deal_id (required)\n * @param string[] $properties A comma separated list of the properties to be returned in the response. If any of the specified properties are not present on the requested object(s), they will be ignored. (optional)\n * @param string[] $associations A comma separated list of object types to retrieve associated IDs for. If any of the specified associations do not exist, they will be ignored. (optional)\n * @param bool $archived Whether to return only results that have been archived. (optional, default to false)\n * @param string $id_property The name of a property whose values are unique for this object type (optional)\n *\n * @throws \\HubSpot\\Client\\Crm\\Deals\\ApiException on non-2xx response\n * @throws \\InvalidArgumentException\n * @return \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations|\\HubSpot\\Client\\Crm\\Deals\\Model\\Error\n */\n public function getById($deal_id, $properties = null, $associations = null, $archived = false, $id_property = null)\n {\n list($response) = $this->getByIdWithHttpInfo($deal_id, $properties, $associations, $archived, $id_property);\n return $response;\n }\n\n /**\n * Operation getByIdWithHttpInfo\n *\n * Read\n *\n * @param string $deal_id (required)\n * @param string[] $properties A comma separated list of the properties to be returned in the response. If any of the specified properties are not present on the requested object(s), they will be ignored. (optional)\n * @param string[] $associations A comma separated list of object types to retrieve associated IDs for. If any of the specified associations do not exist, they will be ignored. (optional)\n * @param bool $archived Whether to return only results that have been archived. (optional, default to false)\n * @param string $id_property The name of a property whose values are unique for this object type (optional)\n *\n * @throws \\HubSpot\\Client\\Crm\\Deals\\ApiException on non-2xx response\n * @throws \\InvalidArgumentException\n * @return array of \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations|\\HubSpot\\Client\\Crm\\Deals\\Model\\Error, HTTP status code, HTTP response headers (array of strings)\n */\n public function getByIdWithHttpInfo($deal_id, $properties = null, $associations = null, $archived = false, $id_property = null)\n {\n $request = $this->getByIdRequest($deal_id, $properties, $associations, $archived, $id_property);\n\n try {\n $options = $this->createHttpClientOption();\n try {\n $response = $this->client->send($request, $options);\n } catch (RequestException $e) {\n throw new ApiException(\n \"[{$e->getCode()}] {$e->getMessage()}\",\n (int) $e->getCode(),\n $e->getResponse() ? $e->getResponse()->getHeaders() : null,\n $e->getResponse() ? (string) $e->getResponse()->getBody() : null\n );\n } catch (ConnectException $e) {\n throw new ApiException(\n \"[{$e->getCode()}] {$e->getMessage()}\",\n (int) $e->getCode(),\n null,\n null\n );\n }\n\n $statusCode = $response->getStatusCode();\n\n if ($statusCode < 200 || $statusCode > 299) {\n throw new ApiException(\n sprintf(\n '[%d] Error connecting to the API (%s)',\n $statusCode,\n (string) $request->getUri()\n ),\n $statusCode,\n $response->getHeaders(),\n (string) $response->getBody()\n );\n }\n\n switch($statusCode) {\n case 200:\n if ('\\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations' === '\\SplFileObject') {\n $content = $response->getBody(); //stream goes to serializer\n } else {\n $content = (string) $response->getBody();\n }\n\n return [\n ObjectSerializer::deserialize($content, '\\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations', []),\n $response->getStatusCode(),\n $response->getHeaders()\n ];\n default:\n if ('\\HubSpot\\Client\\Crm\\Deals\\Model\\Error' === '\\SplFileObject') {\n $content = $response->getBody(); //stream goes to serializer\n } else {\n $content = (string) $response->getBody();\n }\n\n return [\n ObjectSerializer::deserialize($content, '\\HubSpot\\Client\\Crm\\Deals\\Model\\Error', []),\n $response->getStatusCode(),\n $response->getHeaders()\n ];\n }\n\n $returnType = '\\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations';\n if ($returnType === '\\SplFileObject') {\n $content = $response->getBody(); //stream goes to serializer\n } else {\n $content = (string) $response->getBody();\n }\n\n return [\n ObjectSerializer::deserialize($content, $returnType, []),\n $response->getStatusCode(),\n $response->getHeaders()\n ];\n\n } catch (ApiException $e) {\n switch ($e->getCode()) {\n case 200:\n $data = ObjectSerializer::deserialize(\n $e->getResponseBody(),\n '\\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations',\n $e->getResponseHeaders()\n );\n $e->setResponseObject($data);\n break;\n default:\n $data = ObjectSerializer::deserialize(\n $e->getResponseBody(),\n '\\HubSpot\\Client\\Crm\\Deals\\Model\\Error',\n $e->getResponseHeaders()\n );\n $e->setResponseObject($data);\n break;\n }\n throw $e;\n }\n }\n\n /**\n * Operation getByIdAsync\n *\n * Read\n *\n * @param string $deal_id (required)\n * @param string[] $properties A comma separated list of the properties to be returned in the response. If any of the specified properties are not present on the requested object(s), they will be ignored. (optional)\n * @param string[] $associations A comma separated list of object types to retrieve associated IDs for. If any of the specified associations do not exist, they will be ignored. (optional)\n * @param bool $archived Whether to return only results that have been archived. (optional, default to false)\n * @param string $id_property The name of a property whose values are unique for this object type (optional)\n *\n * @throws \\InvalidArgumentException\n * @return \\GuzzleHttp\\Promise\\PromiseInterface\n */\n public function getByIdAsync($deal_id, $properties = null, $associations = null, $archived = false, $id_property = null)\n {\n return $this->getByIdAsyncWithHttpInfo($deal_id, $properties, $associations, $archived, $id_property)\n ->then(\n function ($response) {\n return $response[0];\n }\n );\n }\n\n /**\n * Operation getByIdAsyncWithHttpInfo\n *\n * Read\n *\n * @param string $deal_id (required)\n * @param string[] $properties A comma separated list of the properties to be returned in the response. If any of the specified properties are not present on the requested object(s), they will be ignored. (optional)\n * @param string[] $associations A comma separated list of object types to retrieve associated IDs for. If any of the specified associations do not exist, they will be ignored. (optional)\n * @param bool $archived Whether to return only results that have been archived. (optional, default to false)\n * @param string $id_property The name of a property whose values are unique for this object type (optional)\n *\n * @throws \\InvalidArgumentException\n * @return \\GuzzleHttp\\Promise\\PromiseInterface\n */\n public function getByIdAsyncWithHttpInfo($deal_id, $properties = null, $associations = null, $archived = false, $id_property = null)\n {\n $returnType = '\\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations';\n $request = $this->getByIdRequest($deal_id, $properties, $associations, $archived, $id_property);\n\n return $this->client\n ->sendAsync($request, $this->createHttpClientOption())\n ->then(\n function ($response) use ($returnType) {\n if ($returnType === '\\SplFileObject') {\n $content = $response->getBody(); //stream goes to serializer\n } else {\n $content = (string) $response->getBody();\n }\n\n return [\n ObjectSerializer::deserialize($content, $returnType, []),\n $response->getStatusCode(),\n $response->getHeaders()\n ];\n },\n function ($exception) {\n $response = $exception->getResponse();\n $statusCode = $response->getStatusCode();\n throw new ApiException(\n sprintf(\n '[%d] Error connecting to the API (%s)',\n $statusCode,\n $exception->getRequest()->getUri()\n ),\n $statusCode,\n $response->getHeaders(),\n (string) $response->getBody()\n );\n }\n );\n }\n\n /**\n * Create request for operation 'getById'\n *\n * @param string $deal_id (required)\n * @param string[] $properties A comma separated list of the properties to be returned in the response. If any of the specified properties are not present on the requested object(s), they will be ignored. (optional)\n * @param string[] $associations A comma separated list of object types to retrieve associated IDs for. If any of the specified associations do not exist, they will be ignored. (optional)\n * @param bool $archived Whether to return only results that have been archived. (optional, default to false)\n * @param string $id_property The name of a property whose values are unique for this object type (optional)\n *\n * @throws \\InvalidArgumentException\n * @return \\GuzzleHttp\\Psr7\\Request\n */\n public function getByIdRequest($deal_id, $properties = null, $associations = null, $archived = false, $id_property = null)\n {\n // verify the required parameter 'deal_id' is set\n if ($deal_id === null || (is_array($deal_id) && count($deal_id) === 0)) {\n throw new \\InvalidArgumentException(\n 'Missing the required parameter $deal_id when calling getById'\n );\n }\n\n $resourcePath = '/crm/v3/objects/deals/{dealId}';\n $formParams = [];\n $queryParams = [];\n $headerParams = [];\n $httpBody = '';\n $multipart = false;\n\n // query params\n if ($properties !== null) {\n if('form' === 'form' && is_array($properties)) {\n foreach($properties as $key => $value) {\n $queryParams[$key] = $value;\n }\n }\n else {\n $queryParams['properties'] = $properties;\n }\n }\n // query params\n if ($associations !== null) {\n if('form' === 'form' && is_array($associations)) {\n foreach($associations as $key => $value) {\n $queryParams[$key] = $value;\n }\n }\n else {\n $queryParams['associations'] = $associations;\n }\n }\n // query params\n if ($archived !== null) {\n if('form' === 'form' && is_array($archived)) {\n foreach($archived as $key => $value) {\n $queryParams[$key] = $value;\n }\n }\n else {\n $queryParams['archived'] = $archived;\n }\n }\n // query params\n if ($id_property !== null) {\n if('form' === 'form' && is_array($id_property)) {\n foreach($id_property as $key => $value) {\n $queryParams[$key] = $value;\n }\n }\n else {\n $queryParams['idProperty'] = $id_property;\n }\n }\n\n\n // path params\n if ($deal_id !== null) {\n $resourcePath = str_replace(\n '{' . 'dealId' . '}',\n ObjectSerializer::toPathValue($deal_id),\n $resourcePath\n );\n }\n\n\n if ($multipart) {\n $headers = $this->headerSelector->selectHeadersForMultipart(\n ['application/json', '*/*']\n );\n } else {\n $headers = $this->headerSelector->selectHeaders(\n ['application/json', '*/*'],\n []\n );\n }\n\n // for model (json/xml)\n if (count($formParams) > 0) {\n if ($multipart) {\n $multipartContents = [];\n foreach ($formParams as $formParamName => $formParamValue) {\n $formParamValueItems = is_array($formParamValue) ? $formParamValue : [$formParamValue];\n foreach ($formParamValueItems as $formParamValueItem) {\n $multipartContents[] = [\n 'name' => $formParamName,\n 'contents' => $formParamValueItem\n ];\n }\n }\n // for HTTP post (form)\n $httpBody = new MultipartStream($multipartContents);\n\n } elseif ($headers['Content-Type'] === 'application/json') {\n $httpBody = \\GuzzleHttp\\json_encode($formParams);\n\n } else {\n // for HTTP post (form)\n $httpBody = \\GuzzleHttp\\Psr7\\Query::build($formParams);\n }\n }\n\n // this endpoint requires API key authentication\n $apiKey = $this->config->getApiKeyWithPrefix('hapikey');\n if ($apiKey !== null) {\n $queryParams['hapikey'] = $apiKey;\n }\n // this endpoint requires OAuth (access token)\n if ($this->config->getAccessToken() !== null) {\n $headers['Authorization'] = 'Bearer ' . $this->config->getAccessToken();\n }\n\n $defaultHeaders = [];\n if ($this->config->getUserAgent()) {\n $defaultHeaders['User-Agent'] = $this->config->getUserAgent();\n }\n\n $headers = array_merge(\n $defaultHeaders,\n $headerParams,\n $headers\n );\n\n $query = \\GuzzleHttp\\Psr7\\Query::build($queryParams);\n return new Request(\n 'GET',\n $this->config->getHost() . $resourcePath . ($query ? \"?{$query}\" : ''),\n $headers,\n $httpBody\n );\n }\n\n /**\n * Operation getPage\n *\n * List\n *\n * @param int $limit The maximum number of results to display per page. (optional, default to 10)\n * @param string $after The paging cursor token of the last successfully read resource will be returned as the `paging.next.after` JSON property of a paged response containing more results. (optional)\n * @param string[] $properties A comma separated list of the properties to be returned in the response. If any of the specified properties are not present on the requested object(s), they will be ignored. (optional)\n * @param string[] $associations A comma separated list of object types to retrieve associated IDs for. If any of the specified associations do not exist, they will be ignored. (optional)\n * @param bool $archived Whether to return only results that have been archived. (optional, default to false)\n *\n * @throws \\HubSpot\\Client\\Crm\\Deals\\ApiException on non-2xx response\n * @throws \\InvalidArgumentException\n * @return \\HubSpot\\Client\\Crm\\Deals\\Model\\CollectionResponseSimplePublicObjectWithAssociationsForwardPaging|\\HubSpot\\Client\\Crm\\Deals\\Model\\Error\n */\n public function getPage($limit = 10, $after = null, $properties = null, $associations = null, $archived = false)\n {\n list($response) = $this->getPageWithHttpInfo($limit, $after, $properties, $associations, $archived);\n return $response;\n }\n\n /**\n * Operation getPageWithHttpInfo\n *\n * List\n *\n * @param int $limit The maximum number of results to display per page. (optional, default to 10)\n * @param string $after The paging cursor token of the last successfully read resource will be returned as the `paging.next.after` JSON property of a paged response containing more results. (optional)\n * @param string[] $properties A comma separated list of the properties to be returned in the response. If any of the specified properties are not present on the requested object(s), they will be ignored. (optional)\n * @param string[] $associations A comma separated list of object types to retrieve associated IDs for. If any of the specified associations do not exist, they will be ignored. (optional)\n * @param bool $archived Whether to return only results that have been archived. (optional, default to false)\n *\n * @throws \\HubSpot\\Client\\Crm\\Deals\\ApiException on non-2xx response\n * @throws \\InvalidArgumentException\n * @return array of \\HubSpot\\Client\\Crm\\Deals\\Model\\CollectionResponseSimplePublicObjectWithAssociationsForwardPaging|\\HubSpot\\Client\\Crm\\Deals\\Model\\Error, HTTP status code, HTTP response headers (array of strings)\n */\n public function getPageWithHttpInfo($limit = 10, $after = null, $properties = null, $associations = null, $archived = false)\n {\n $request = $this->getPageRequest($limit, $after, $properties, $associations, $archived);\n\n try {\n $options = $this->createHttpClientOption();\n try {\n $response = $this->client->send($request, $options);\n } catch (RequestException $e) {\n throw new ApiException(\n \"[{$e->getCode()}] {$e->getMessage()}\",\n (int) $e->getCode(),\n $e->getResponse() ? $e->getResponse()->getHeaders() : null,\n $e->getResponse() ? (string) $e->getResponse()->getBody() : null\n );\n } catch (ConnectException $e) {\n throw new ApiException(\n \"[{$e->getCode()}] {$e->getMessage()}\",\n (int) $e->getCode(),\n null,\n null\n );\n }\n\n $statusCode = $response->getStatusCode();\n\n if ($statusCode < 200 || $statusCode > 299) {\n throw new ApiException(\n sprintf(\n '[%d] Error connecting to the API (%s)',\n $statusCode,\n (string) $request->getUri()\n ),\n $statusCode,\n $response->getHeaders(),\n (string) $response->getBody()\n );\n }\n\n switch($statusCode) {\n case 200:\n if ('\\HubSpot\\Client\\Crm\\Deals\\Model\\CollectionResponseSimplePublicObjectWithAssociationsForwardPaging' === '\\SplFileObject') {\n $content = $response->getBody(); //stream goes to serializer\n } else {\n $content = (string) $response->getBody();\n }\n\n return [\n ObjectSerializer::deserialize($content, '\\HubSpot\\Client\\Crm\\Deals\\Model\\CollectionResponseSimplePublicObjectWithAssociationsForwardPaging', []),\n $response->getStatusCode(),\n $response->getHeaders()\n ];\n default:\n if ('\\HubSpot\\Client\\Crm\\Deals\\Model\\Error' === '\\SplFileObject') {\n $content = $response->getBody(); //stream goes to serializer\n } else {\n $content = (string) $response->getBody();\n }\n\n return [\n ObjectSerializer::deserialize($content, '\\HubSpot\\Client\\Crm\\Deals\\Model\\Error', []),\n $response->getStatusCode(),\n $response->getHeaders()\n ];\n }\n\n $returnType = '\\HubSpot\\Client\\Crm\\Deals\\Model\\CollectionResponseSimplePublicObjectWithAssociationsForwardPaging';\n if ($returnType === '\\SplFileObject') {\n $content = $response->getBody(); //stream goes to serializer\n } else {\n $content = (string) $response->getBody();\n }\n\n return [\n ObjectSerializer::deserialize($content, $returnType, []),\n $response->getStatusCode(),\n $response->getHeaders()\n ];\n\n } catch (ApiException $e) {\n switch ($e->getCode()) {\n case 200:\n $data = ObjectSerializer::deserialize(\n $e->getResponseBody(),\n '\\HubSpot\\Client\\Crm\\Deals\\Model\\CollectionResponseSimplePublicObjectWithAssociationsForwardPaging',\n $e->getResponseHeaders()\n );\n $e->setResponseObject($data);\n break;\n default:\n $data = ObjectSerializer::deserialize(\n $e->getResponseBody(),\n '\\HubSpot\\Client\\Crm\\Deals\\Model\\Error',\n $e->getResponseHeaders()\n );\n $e->setResponseObject($data);\n break;\n }\n throw $e;\n }\n }\n\n /**\n * Operation getPageAsync\n *\n * List\n *\n * @param int $limit The maximum number of results to display per page. (optional, default to 10)\n * @param string $after The paging cursor token of the last successfully read resource will be returned as the `paging.next.after` JSON property of a paged response containing more results. (optional)\n * @param string[] $properties A comma separated list of the properties to be returned in the response. If any of the specified properties are not present on the requested object(s), they will be ignored. (optional)\n * @param string[] $associations A comma separated list of object types to retrieve associated IDs for. If any of the specified associations do not exist, they will be ignored. (optional)\n * @param bool $archived Whether to return only results that have been archived. (optional, default to false)\n *\n * @throws \\InvalidArgumentException\n * @return \\GuzzleHttp\\Promise\\PromiseInterface\n */\n public function getPageAsync($limit = 10, $after = null, $properties = null, $associations = null, $archived = false)\n {\n return $this->getPageAsyncWithHttpInfo($limit, $after, $properties, $associations, $archived)\n ->then(\n function ($response) {\n return $response[0];\n }\n );\n }\n\n /**\n * Operation getPageAsyncWithHttpInfo\n *\n * List\n *\n * @param int $limit The maximum number of results to display per page. (optional, default to 10)\n * @param string $after The paging cursor token of the last successfully read resource will be returned as the `paging.next.after` JSON property of a paged response containing more results. (optional)\n * @param string[] $properties A comma separated list of the properties to be returned in the response. If any of the specified properties are not present on the requested object(s), they will be ignored. (optional)\n * @param string[] $associations A comma separated list of object types to retrieve associated IDs for. If any of the specified associations do not exist, they will be ignored. (optional)\n * @param bool $archived Whether to return only results that have been archived. (optional, default to false)\n *\n * @throws \\InvalidArgumentException\n * @return \\GuzzleHttp\\Promise\\PromiseInterface\n */\n public function getPageAsyncWithHttpInfo($limit = 10, $after = null, $properties = null, $associations = null, $archived = false)\n {\n $returnType = '\\HubSpot\\Client\\Crm\\Deals\\Model\\CollectionResponseSimplePublicObjectWithAssociationsForwardPaging';\n $request = $this->getPageRequest($limit, $after, $properties, $associations, $archived);\n\n return $this->client\n ->sendAsync($request, $this->createHttpClientOption())\n ->then(\n function ($response) use ($returnType) {\n if ($returnType === '\\SplFileObject') {\n $content = $response->getBody(); //stream goes to serializer\n } else {\n $content = (string) $response->getBody();\n }\n\n return [\n ObjectSerializer::deserialize($content, $returnType, []),\n $response->getStatusCode(),\n $response->getHeaders()\n ];\n },\n function ($exception) {\n $response = $exception->getResponse();\n $statusCode = $response->getStatusCode();\n throw new ApiException(\n sprintf(\n '[%d] Error connecting to the API (%s)',\n $statusCode,\n $exception->getRequest()->getUri()\n ),\n $statusCode,\n $response->getHeaders(),\n (string) $response->getBody()\n );\n }\n );\n }\n\n /**\n * Create request for operation 'getPage'\n *\n * @param int $limit The maximum number of results to display per page. (optional, default to 10)\n * @param string $after The paging cursor token of the last successfully read resource will be returned as the `paging.next.after` JSON property of a paged response containing more results. (optional)\n * @param string[] $properties A comma separated list of the properties to be returned in the response. If any of the specified properties are not present on the requested object(s), they will be ignored. (optional)\n * @param string[] $associations A comma separated list of object types to retrieve associated IDs for. If any of the specified associations do not exist, they will be ignored. (optional)\n * @param bool $archived Whether to return only results that have been archived. (optional, default to false)\n *\n * @throws \\InvalidArgumentException\n * @return \\GuzzleHttp\\Psr7\\Request\n */\n public function getPageRequest($limit = 10, $after = null, $properties = null, $associations = null, $archived = false)\n {\n\n $resourcePath = '/crm/v3/objects/deals';\n $formParams = [];\n $queryParams = [];\n $headerParams = [];\n $httpBody = '';\n $multipart = false;\n\n // query params\n if ($limit !== null) {\n if('form' === 'form' && is_array($limit)) {\n foreach($limit as $key => $value) {\n $queryParams[$key] = $value;\n }\n }\n else {\n $queryParams['limit'] = $limit;\n }\n }\n // query params\n if ($after !== null) {\n if('form' === 'form' && is_array($after)) {\n foreach($after as $key => $value) {\n $queryParams[$key] = $value;\n }\n }\n else {\n $queryParams['after'] = $after;\n }\n }\n // query params\n if ($properties !== null) {\n if('form' === 'form' && is_array($properties)) {\n foreach($properties as $key => $value) {\n $queryParams[$key] = $value;\n }\n }\n else {\n $queryParams['properties'] = $properties;\n }\n }\n // query params\n if ($associations !== null) {\n if('form' === 'form' && is_array($associations)) {\n foreach($associations as $key => $value) {\n $queryParams[$key] = $value;\n }\n }\n else {\n $queryParams['associations'] = $associations;\n }\n }\n // query params\n if ($archived !== null) {\n if('form' === 'form' && is_array($archived)) {\n foreach($archived as $key => $value) {\n $queryParams[$key] = $value;\n }\n }\n else {\n $queryParams['archived'] = $archived;\n }\n }\n\n\n\n\n if ($multipart) {\n $headers = $this->headerSelector->selectHeadersForMultipart(\n ['application/json', '*/*']\n );\n } else {\n $headers = $this->headerSelector->selectHeaders(\n ['application/json', '*/*'],\n []\n );\n }\n\n // for model (json/xml)\n if (count($formParams) > 0) {\n if ($multipart) {\n $multipartContents = [];\n foreach ($formParams as $formParamName => $formParamValue) {\n $formParamValueItems = is_array($formParamValue) ? $formParamValue : [$formParamValue];\n foreach ($formParamValueItems as $formParamValueItem) {\n $multipartContents[] = [\n 'name' => $formParamName,\n 'contents' => $formParamValueItem\n ];\n }\n }\n // for HTTP post (form)\n $httpBody = new MultipartStream($multipartContents);\n\n } elseif ($headers['Content-Type'] === 'application/json') {\n $httpBody = \\GuzzleHttp\\json_encode($formParams);\n\n } else {\n // for HTTP post (form)\n $httpBody = \\GuzzleHttp\\Psr7\\Query::build($formParams);\n }\n }\n\n // this endpoint requires API key authentication\n $apiKey = $this->config->getApiKeyWithPrefix('hapikey');\n if ($apiKey !== null) {\n $queryParams['hapikey'] = $apiKey;\n }\n // this endpoint requires OAuth (access token)\n if ($this->config->getAccessToken() !== null) {\n $headers['Authorization'] = 'Bearer ' . $this->config->getAccessToken();\n }\n\n $defaultHeaders = [];\n if ($this->config->getUserAgent()) {\n $defaultHeaders['User-Agent'] = $this->config->getUserAgent();\n }\n\n $headers = array_merge(\n $defaultHeaders,\n $headerParams,\n $headers\n );\n\n $query = \\GuzzleHttp\\Psr7\\Query::build($queryParams);\n return new Request(\n 'GET',\n $this->config->getHost() . $resourcePath . ($query ? \"?{$query}\" : ''),\n $headers,\n $httpBody\n );\n }\n\n /**\n * Operation update\n *\n * Update\n *\n * @param string $deal_id deal_id (required)\n * @param \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectInput $simple_public_object_input simple_public_object_input (required)\n * @param string $id_property The name of a property whose values are unique for this object type (optional)\n *\n * @throws \\HubSpot\\Client\\Crm\\Deals\\ApiException on non-2xx response\n * @throws \\InvalidArgumentException\n * @return \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObject|\\HubSpot\\Client\\Crm\\Deals\\Model\\Error\n */\n public function update($deal_id, $simple_public_object_input, $id_property = null)\n {\n list($response) = $this->updateWithHttpInfo($deal_id, $simple_public_object_input, $id_property);\n return $response;\n }\n\n /**\n * Operation updateWithHttpInfo\n *\n * Update\n *\n * @param string $deal_id (required)\n * @param \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectInput $simple_public_object_input (required)\n * @param string $id_property The name of a property whose values are unique for this object type (optional)\n *\n * @throws \\HubSpot\\Client\\Crm\\Deals\\ApiException on non-2xx response\n * @throws \\InvalidArgumentException\n * @return array of \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObject|\\HubSpot\\Client\\Crm\\Deals\\Model\\Error, HTTP status code, HTTP response headers (array of strings)\n */\n public function updateWithHttpInfo($deal_id, $simple_public_object_input, $id_property = null)\n {\n $request = $this->updateRequest($deal_id, $simple_public_object_input, $id_property);\n\n try {\n $options = $this->createHttpClientOption();\n try {\n $response = $this->client->send($request, $options);\n } catch (RequestException $e) {\n throw new ApiException(\n \"[{$e->getCode()}] {$e->getMessage()}\",\n (int) $e->getCode(),\n $e->getResponse() ? $e->getResponse()->getHeaders() : null,\n $e->getResponse() ? (string) $e->getResponse()->getBody() : null\n );\n } catch (ConnectException $e) {\n throw new ApiException(\n \"[{$e->getCode()}] {$e->getMessage()}\",\n (int) $e->getCode(),\n null,\n null\n );\n }\n\n $statusCode = $response->getStatusCode();\n\n if ($statusCode < 200 || $statusCode > 299) {\n throw new ApiException(\n sprintf(\n '[%d] Error connecting to the API (%s)',\n $statusCode,\n (string) $request->getUri()\n ),\n $statusCode,\n $response->getHeaders(),\n (string) $response->getBody()\n );\n }\n\n switch($statusCode) {\n case 200:\n if ('\\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObject' === '\\SplFileObject') {\n $content = $response->getBody(); //stream goes to serializer\n } else {\n $content = (string) $response->getBody();\n }\n\n return [\n ObjectSerializer::deserialize($content, '\\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObject', []),\n $response->getStatusCode(),\n $response->getHeaders()\n ];\n default:\n if ('\\HubSpot\\Client\\Crm\\Deals\\Model\\Error' === '\\SplFileObject') {\n $content = $response->getBody(); //stream goes to serializer\n } else {\n $content = (string) $response->getBody();\n }\n\n return [\n ObjectSerializer::deserialize($content, '\\HubSpot\\Client\\Crm\\Deals\\Model\\Error', []),\n $response->getStatusCode(),\n $response->getHeaders()\n ];\n }\n\n $returnType = '\\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObject';\n if ($returnType === '\\SplFileObject') {\n $content = $response->getBody(); //stream goes to serializer\n } else {\n $content = (string) $response->getBody();\n }\n\n return [\n ObjectSerializer::deserialize($content, $returnType, []),\n $response->getStatusCode(),\n $response->getHeaders()\n ];\n\n } catch (ApiException $e) {\n switch ($e->getCode()) {\n case 200:\n $data = ObjectSerializer::deserialize(\n $e->getResponseBody(),\n '\\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObject',\n $e->getResponseHeaders()\n );\n $e->setResponseObject($data);\n break;\n default:\n $data = ObjectSerializer::deserialize(\n $e->getResponseBody(),\n '\\HubSpot\\Client\\Crm\\Deals\\Model\\Error',\n $e->getResponseHeaders()\n );\n $e->setResponseObject($data);\n break;\n }\n throw $e;\n }\n }\n\n /**\n * Operation updateAsync\n *\n * Update\n *\n * @param string $deal_id (required)\n * @param \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectInput $simple_public_object_input (required)\n * @param string $id_property The name of a property whose values are unique for this object type (optional)\n *\n * @throws \\InvalidArgumentException\n * @return \\GuzzleHttp\\Promise\\PromiseInterface\n */\n public function updateAsync($deal_id, $simple_public_object_input, $id_property = null)\n {\n return $this->updateAsyncWithHttpInfo($deal_id, $simple_public_object_input, $id_property)\n ->then(\n function ($response) {\n return $response[0];\n }\n );\n }\n\n /**\n * Operation updateAsyncWithHttpInfo\n *\n * Update\n *\n * @param string $deal_id (required)\n * @param \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectInput $simple_public_object_input (required)\n * @param string $id_property The name of a property whose values are unique for this object type (optional)\n *\n * @throws \\InvalidArgumentException\n * @return \\GuzzleHttp\\Promise\\PromiseInterface\n */\n public function updateAsyncWithHttpInfo($deal_id, $simple_public_object_input, $id_property = null)\n {\n $returnType = '\\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObject';\n $request = $this->updateRequest($deal_id, $simple_public_object_input, $id_property);\n\n return $this->client\n ->sendAsync($request, $this->createHttpClientOption())\n ->then(\n function ($response) use ($returnType) {\n if ($returnType === '\\SplFileObject') {\n $content = $response->getBody(); //stream goes to serializer\n } else {\n $content = (string) $response->getBody();\n }\n\n return [\n ObjectSerializer::deserialize($content, $returnType, []),\n $response->getStatusCode(),\n $response->getHeaders()\n ];\n },\n function ($exception) {\n $response = $exception->getResponse();\n $statusCode = $response->getStatusCode();\n throw new ApiException(\n sprintf(\n '[%d] Error connecting to the API (%s)',\n $statusCode,\n $exception->getRequest()->getUri()\n ),\n $statusCode,\n $response->getHeaders(),\n (string) $response->getBody()\n );\n }\n );\n }\n\n /**\n * Create request for operation 'update'\n *\n * @param string $deal_id (required)\n * @param \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectInput $simple_public_object_input (required)\n * @param string $id_property The name of a property whose values are unique for this object type (optional)\n *\n * @throws \\InvalidArgumentException\n * @return \\GuzzleHttp\\Psr7\\Request\n */\n public function updateRequest($deal_id, $simple_public_object_input, $id_property = null)\n {\n // verify the required parameter 'deal_id' is set\n if ($deal_id === null || (is_array($deal_id) && count($deal_id) === 0)) {\n throw new \\InvalidArgumentException(\n 'Missing the required parameter $deal_id when calling update'\n );\n }\n // verify the required parameter 'simple_public_object_input' is set\n if ($simple_public_object_input === null || (is_array($simple_public_object_input) && count($simple_public_object_input) === 0)) {\n throw new \\InvalidArgumentException(\n 'Missing the required parameter $simple_public_object_input when calling update'\n );\n }\n\n $resourcePath = '/crm/v3/objects/deals/{dealId}';\n $formParams = [];\n $queryParams = [];\n $headerParams = [];\n $httpBody = '';\n $multipart = false;\n\n // query params\n if ($id_property !== null) {\n if('form' === 'form' && is_array($id_property)) {\n foreach($id_property as $key => $value) {\n $queryParams[$key] = $value;\n }\n }\n else {\n $queryParams['idProperty'] = $id_property;\n }\n }\n\n\n // path params\n if ($deal_id !== null) {\n $resourcePath = str_replace(\n '{' . 'dealId' . '}',\n ObjectSerializer::toPathValue($deal_id),\n $resourcePath\n );\n }\n\n\n if ($multipart) {\n $headers = $this->headerSelector->selectHeadersForMultipart(\n ['application/json', '*/*']\n );\n } else {\n $headers = $this->headerSelector->selectHeaders(\n ['application/json', '*/*'],\n ['application/json']\n );\n }\n\n // for model (json/xml)\n if (isset($simple_public_object_input)) {\n if ($headers['Content-Type'] === 'application/json') {\n $httpBody = \\GuzzleHttp\\json_encode(ObjectSerializer::sanitizeForSerialization($simple_public_object_input));\n } else {\n $httpBody = $simple_public_object_input;\n }\n } elseif (count($formParams) > 0) {\n if ($multipart) {\n $multipartContents = [];\n foreach ($formParams as $formParamName => $formParamValue) {\n $formParamValueItems = is_array($formParamValue) ? $formParamValue : [$formParamValue];\n foreach ($formParamValueItems as $formParamValueItem) {\n $multipartContents[] = [\n 'name' => $formParamName,\n 'contents' => $formParamValueItem\n ];\n }\n }\n // for HTTP post (form)\n $httpBody = new MultipartStream($multipartContents);\n\n } elseif ($headers['Content-Type'] === 'application/json') {\n $httpBody = \\GuzzleHttp\\json_encode($formParams);\n\n } else {\n // for HTTP post (form)\n $httpBody = \\GuzzleHttp\\Psr7\\Query::build($formParams);\n }\n }\n\n // this endpoint requires API key authentication\n $apiKey = $this->config->getApiKeyWithPrefix('hapikey');\n if ($apiKey !== null) {\n $queryParams['hapikey'] = $apiKey;\n }\n // this endpoint requires OAuth (access token)\n if ($this->config->getAccessToken() !== null) {\n $headers['Authorization'] = 'Bearer ' . $this->config->getAccessToken();\n }\n\n $defaultHeaders = [];\n if ($this->config->getUserAgent()) {\n $defaultHeaders['User-Agent'] = $this->config->getUserAgent();\n }\n\n $headers = array_merge(\n $defaultHeaders,\n $headerParams,\n $headers\n );\n\n $query = \\GuzzleHttp\\Psr7\\Query::build($queryParams);\n return new Request(\n 'PATCH',\n $this->config->getHost() . $resourcePath . ($query ? \"?{$query}\" : ''),\n $headers,\n $httpBody\n );\n }\n\n /**\n * Create http client option\n *\n * @throws \\RuntimeException on file opening failure\n * @return array of http client options\n */\n protected function createHttpClientOption()\n {\n $options = [];\n if ($this->config->getDebug()) {\n $options[RequestOptions::DEBUG] = fopen($this->config->getDebugFile(), 'a');\n if (!$options[RequestOptions::DEBUG]) {\n throw new \\RuntimeException('Failed to open the debug file: ' . $this->config->getDebugFile());\n }\n }\n\n return $options;\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"app ~/jiminny/app, folder","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".circleci, folder","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".cursor, folder","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".github","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".sonarlint, folder","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".vscode, folder","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".windsurf, folder","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"app, sources root","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Actions, folder","depth":8,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Component, folder","depth":8,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Acl, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ActionItems, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Activity, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ActivityAnalytics, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ActivitySearch, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"AiActivityType, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"AiAutomation, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"AiCallScoring, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"AskAnything, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Dtos, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Events, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"AskAnythingPromptService.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"HistoryService.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"AskJiminnyAi, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"AWS, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"BillingManagement, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Cache, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"CoachingFeedback, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Country, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"CustomerApi, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Database, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Datadog, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"DateTime, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"DealInsights, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"DealRisks","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ElasticSearch","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Eloquent","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Encoding","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Encryption","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ES","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Faker","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"FeatureFlags","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"FFMpeg","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"FileSystem","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Gecko","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Gong","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"GuzzleHttp","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"KeyPoints","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Kiosk","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"LanguageDetection","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"LiveFeed","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Locks","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Math","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"MediaPipeline","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"MeetingBot, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"MobileSettings, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Model, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Notification, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Nudge, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ParagraphBreaker, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ParticipantSpeech, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"PartitionedCookie, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"PlaybackPage, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Playlist, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Prophet, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ProphetAi, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ProsperWorks, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Queue, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Job, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"RateLimitAware.php, abstract class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"RateLimitAwareWrapper.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"BotsQueueConstants.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Constants.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ProcessingQueueConstants.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Router, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Saml2","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"SCIM","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Seeder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Sentry","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Serializer, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Settings, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Sidekick, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Slack, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"TeamInsights, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"TimeMemoryMapper, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Transcription, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"TranscriptionSummary, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Twilio, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Uploader, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"UrlGenerator, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Utility, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Exceptions, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Service, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"BaseRateLimiter.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"EfficientJsonParser.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ProviderRateLimiter.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"RateLimiterInstance.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Uuid","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Waveform","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Webhooks","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Workflow","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Configuration","depth":8,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Console, folder","depth":8,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Commands, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Activities, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Analytics, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Calendars, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Crm, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Hubspot, folder","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"IntegrationApp, folder","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Traits, folder","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"AddLayoutEntities.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"AutologDelayedCommand.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"BullhornCommandAbstract.php, abstract class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"BullhornPingCommand.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"BullhornSearchCommand.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"BullhornSessionCommand.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"CheckActivityLoggableCommand.php, final class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"CleanDuplicateFieldDataCommand.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"FullSyncOpportunityCommand.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"LogActivitiesCommand.php, final class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ManageSyncStrategyCommand.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"MatchCrmObjectsCommand.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"MatchOpportunityActivitiesCommand.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"MigrateProvider.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ProcessHubspotObjectsSyncBatches.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"PurgeDeletedOpportunitiesCommand.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ResetGovernorLimits.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"SendNotLogged.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"SetupActivityTypeForFollowUp.php, final class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"SetupCloseCrm.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"SetupCopperCrm.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"SetupCrmCommand.php, abstract class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"SetupLayouts.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"SyncAccount.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"SyncContact.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"SyncFieldMetadata.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"SyncHubspotActiveDeals.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"SyncHubspotObjects.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"SyncLead.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"SyncObjects.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"SyncOpportunitiesMissingFieldDataCommand.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"SyncOpportunity.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"SyncProfileMetadata.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"SyncTeamMetadata.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"UpdateOpportunitySpecifications.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"DealInsights, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Dev","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"AddRateLimitCommand.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"FixHubSpotTokens.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"FixMissMatchedCrmActivitiesCommand.php, final class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ImportCallsCommand.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"MonitorSocialAccountsState.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Dialers, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"DTOs, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Elasticsearch","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"EngagementStats","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"GeckoExport","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Livestream","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Mailboxes","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Migrate","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"PlaybackThemes","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Playbooks","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Playlists","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Postmark","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ProphetAi","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Reports","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"AutomatedReportsCommand.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"AutomatedReportsRetentionPolicyCommand.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"AutomatedReportsSendCommand.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"CreateMockAskJiminnyReportResultCommand.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"DeleteReportCommand.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"GenerateMarketingReport.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Team.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Usage.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Slack","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Teams","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Tracks","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Transcription","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Twilio","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Users","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Vocabulary","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Zoom","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Command.php, class","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"CreateDatabaseUsers.php, class","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"DatabaseTableCount.php, class","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"DeleteOldAiCrmNotesCommand.php, class","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"DeleteS3LeftoversCommand.php, class","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"DevPostmanCommand.php, final class","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"DiarizeViaAiParticipantIdentificationCommand.php, class","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"EncryptTokensCommand.php, class","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"EngagementStatsRegenerateCommand.php, class","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"FeatureFlagsHelper.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"FixCrossTenantIssues.php, class","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"FlushRolesPermissionsCache.php, class","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"GenerateInternalWebhookToken.php, class","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"GroupSetDefaultLanguageCommand.php, final class","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"HelperTruncateCoachingTables.php, class","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"HubspotJournalPollingCommand.php, class","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"HubspotWebhookServiceCommand.php, class","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ImportRecording.php, class","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ImportUsersFromCsvFile.php, final class","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"IterateUsersCommand.php, abstract class","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"JiminnyCacheClearCommand.php, class","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"JiminnyDebugCommand.php, class","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"JiminnySetEncryptedTokenManagerModeCommand.php, class","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"JiminnyTokenInfoCommand.php, class","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"MakeSlackLiveCoachingChatNotesOn.php, class","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ManageScimForTeam.php, class","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"MarkBranchForEnvironmentPipelineCommand.php, class","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"MuteOrganizerChannel.php, class","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"PhpApm.php, class","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"PropagateCoachingFeedbackCreatedAtToSectionFeedbacks.php, class","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"PurgeConferences.php, class","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"PurgeSoftDeletedOpportunitiesCommand.php, class","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"PurgeSyncBatchesCommand.php, class","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"RecalculateDealRisksCommand.php, class","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"RemoveDeleteMarkersCommand.php, class","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"RemoveExpiredNudgesCommand.php, class","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"RemoveUnusedParticipantSpeechesCommand.php, class","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ResetElasticSearch.php, class","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"RestoreActivityCrmProviderIdCommand.php, class","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"RestoreActivityTypeCommand.php, class","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"RunAiCallScoringForUntypedActivitiesCommand.php, class","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"SeedActivities.php, class","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"SendNudgeExpirationWarningsCommand.php, class","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"SyncActivity.php, class","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"TrackImported.php, class","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"WhichWorkerIsWorkingOnWhichJob.php, class","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Scheduling, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Kernel.php","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Contracts, folder","depth":8,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Acl, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ActivitySearch, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"AiAutomation, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Crm, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"DateTime, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ES, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Http, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Requests, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ApiResponse.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"RateLimited.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"RateLimitInterface.php","depth":10,"on_screen":false,"role_description":"text"}]...
|
-5057445341270851099
|
252663743049766078
|
visual_change
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
Editor for custom.log
Code changed:
Hide
Sync Changes
Hide This Notification
Reader Mode
<?php
/**
* BasicApi
* PHP version 7.3
*
* @category Class
* @package HubSpot\Client\Crm\Deals
* @author OpenAPI Generator team
* @link [URL_WITH_CREDENTIALS] Class
* @package HubSpot\Client\Crm\Deals
* @author OpenAPI Generator team
* @link [URL_WITH_CREDENTIALS] int $hostIndex Host index (required)
*/
public function setHostIndex($hostIndex): void
{
$this->hostIndex = $hostIndex;
}
/**
* Get the host index
*
* @return int Host index
*/
public function getHostIndex()
{
return $this->hostIndex;
}
/**
* @return Configuration
*/
public function getConfig()
{
return $this->config;
}
/**
* Operation archive
*
* Archive
*
* @param string $deal_id deal_id (required)
*
* @throws \HubSpot\Client\Crm\Deals\ApiException on non-2xx response
* @throws \InvalidArgumentException
* @return void
*/
public function archive($deal_id)
{
$this->archiveWithHttpInfo($deal_id);
}
/**
* Operation archiveWithHttpInfo
*
* Archive
*
* @param string $deal_id (required)
*
* @throws \HubSpot\Client\Crm\Deals\ApiException on non-2xx response
* @throws \InvalidArgumentException
* @return array of null, HTTP status code, HTTP response headers (array of strings)
*/
public function archiveWithHttpInfo($deal_id)
{
$request = $this->archiveRequest($deal_id);
try {
$options = $this->createHttpClientOption();
try {
$response = $this->client->send($request, $options);
} catch (RequestException $e) {
throw new ApiException(
"[{$e->getCode()}] {$e->getMessage()}",
(int) $e->getCode(),
$e->getResponse() ? $e->getResponse()->getHeaders() : null,
$e->getResponse() ? (string) $e->getResponse()->getBody() : null
);
} catch (ConnectException $e) {
throw new ApiException(
"[{$e->getCode()}] {$e->getMessage()}",
(int) $e->getCode(),
null,
null
);
}
$statusCode = $response->getStatusCode();
if ($statusCode < 200 || $statusCode > 299) {
throw new ApiException(
sprintf(
'[%d] Error connecting to the API (%s)',
$statusCode,
(string) $request->getUri()
),
$statusCode,
$response->getHeaders(),
(string) $response->getBody()
);
}
return [null, $statusCode, $response->getHeaders()];
} catch (ApiException $e) {
switch ($e->getCode()) {
default:
$data = ObjectSerializer::deserialize(
$e->getResponseBody(),
'\HubSpot\Client\Crm\Deals\Model\Error',
$e->getResponseHeaders()
);
$e->setResponseObject($data);
break;
}
throw $e;
}
}
/**
* Operation archiveAsync
*
* Archive
*
* @param string $deal_id (required)
*
* @throws \InvalidArgumentException
* @return \GuzzleHttp\Promise\PromiseInterface
*/
public function archiveAsync($deal_id)
{
return $this->archiveAsyncWithHttpInfo($deal_id)
->then(
function ($response) {
return $response[0];
}
);
}
/**
* Operation archiveAsyncWithHttpInfo
*
* Archive
*
* @param string $deal_id (required)
*
* @throws \InvalidArgumentException
* @return \GuzzleHttp\Promise\PromiseInterface
*/
public function archiveAsyncWithHttpInfo($deal_id)
{
$returnType = '';
$request = $this->archiveRequest($deal_id);
return $this->client
->sendAsync($request, $this->createHttpClientOption())
->then(
function ($response) use ($returnType) {
return [null, $response->getStatusCode(), $response->getHeaders()];
},
function ($exception) {
$response = $exception->getResponse();
$statusCode = $response->getStatusCode();
throw new ApiException(
sprintf(
'[%d] Error connecting to the API (%s)',
$statusCode,
$exception->getRequest()->getUri()
),
$statusCode,
$response->getHeaders(),
(string) $response->getBody()
);
}
);
}
/**
* Create request for operation 'archive'
*
* @param string $deal_id (required)
*
* @throws \InvalidArgumentException
* @return \GuzzleHttp\Psr7\Request
*/
public function archiveRequest($deal_id)
{
// verify the required parameter 'deal_id' is set
if ($deal_id === null || (is_array($deal_id) && count($deal_id) === 0)) {
throw new \InvalidArgumentException(
'Missing the required parameter $deal_id when calling archive'
);
}
$resourcePath = '/crm/v3/objects/deals/{dealId}';
$formParams = [];
$queryParams = [];
$headerParams = [];
$httpBody = '';
$multipart = false;
// path params
if ($deal_id !== null) {
$resourcePath = str_replace(
'{' . 'dealId' . '}',
ObjectSerializer::toPathValue($deal_id),
$resourcePath
);
}
if ($multipart) {
$headers = $this->headerSelector->selectHeadersForMultipart(
['*/*']
);
} else {
$headers = $this->headerSelector->selectHeaders(
['*/*'],
[]
);
}
// for model (json/xml)
if (count($formParams) > 0) {
if ($multipart) {
$multipartContents = [];
foreach ($formParams as $formParamName => $formParamValue) {
$formParamValueItems = is_array($formParamValue) ? $formParamValue : [$formParamValue];
foreach ($formParamValueItems as $formParamValueItem) {
$multipartContents[] = [
'name' => $formParamName,
'contents' => $formParamValueItem
];
}
}
// for HTTP post (form)
$httpBody = new MultipartStream($multipartContents);
} elseif ($headers['Content-Type'] === 'application/json') {
$httpBody = \GuzzleHttp\json_encode($formParams);
} else {
// for HTTP post (form)
$httpBody = \GuzzleHttp\Psr7\Query::build($formParams);
}
}
// this endpoint requires API key authentication
$apiKey = $this->config->getApiKeyWithPrefix('hapikey');
if ($apiKey !== null) {
$queryParams['hapikey'] = $apiKey;
}
// this endpoint requires OAuth (access token)
if ($this->config->getAccessToken() !== null) {
$headers['Authorization'] = 'Bearer ' . $this->config->getAccessToken();
}
$defaultHeaders = [];
if ($this->config->getUserAgent()) {
$defaultHeaders['User-Agent'] = $this->config->getUserAgent();
}
$headers = array_merge(
$defaultHeaders,
$headerParams,
$headers
);
$query = \GuzzleHttp\Psr7\Query::build($queryParams);
return new Request(
'DELETE',
$this->config->getHost() . $resourcePath . ($query ? "?{$query}" : ''),
$headers,
$httpBody
);
}
/**
* Operation create
*
* Create
*
* @param \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectInput $simple_public_object_input simple_public_object_input (required)
*
* @throws \HubSpot\Client\Crm\Deals\ApiException on non-2xx response
* @throws \InvalidArgumentException
* @return \HubSpot\Client\Crm\Deals\Model\SimplePublicObject|\HubSpot\Client\Crm\Deals\Model\Error
*/
public function create($simple_public_object_input)
{
list($response) = $this->createWithHttpInfo($simple_public_object_input);
return $response;
}
/**
* Operation createWithHttpInfo
*
* Create
*
* @param \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectInput $simple_public_object_input (required)
*
* @throws \HubSpot\Client\Crm\Deals\ApiException on non-2xx response
* @throws \InvalidArgumentException
* @return array of \HubSpot\Client\Crm\Deals\Model\SimplePublicObject|\HubSpot\Client\Crm\Deals\Model\Error, HTTP status code, HTTP response headers (array of strings)
*/
public function createWithHttpInfo($simple_public_object_input)
{
$request = $this->createRequest($simple_public_object_input);
try {
$options = $this->createHttpClientOption();
try {
$response = $this->client->send($request, $options);
} catch (RequestException $e) {
throw new ApiException(
"[{$e->getCode()}] {$e->getMessage()}",
(int) $e->getCode(),
$e->getResponse() ? $e->getResponse()->getHeaders() : null,
$e->getResponse() ? (string) $e->getResponse()->getBody() : null
);
} catch (ConnectException $e) {
throw new ApiException(
"[{$e->getCode()}] {$e->getMessage()}",
(int) $e->getCode(),
null,
null
);
}
$statusCode = $response->getStatusCode();
if ($statusCode < 200 || $statusCode > 299) {
throw new ApiException(
sprintf(
'[%d] Error connecting to the API (%s)',
$statusCode,
(string) $request->getUri()
),
$statusCode,
$response->getHeaders(),
(string) $response->getBody()
);
}
switch($statusCode) {
case 201:
if ('\HubSpot\Client\Crm\Deals\Model\SimplePublicObject' === '\SplFileObject') {
$content = $response->getBody(); //stream goes to serializer
} else {
$content = (string) $response->getBody();
}
return [
ObjectSerializer::deserialize($content, '\HubSpot\Client\Crm\Deals\Model\SimplePublicObject', []),
$response->getStatusCode(),
$response->getHeaders()
];
default:
if ('\HubSpot\Client\Crm\Deals\Model\Error' === '\SplFileObject') {
$content = $response->getBody(); //stream goes to serializer
} else {
$content = (string) $response->getBody();
}
return [
ObjectSerializer::deserialize($content, '\HubSpot\Client\Crm\Deals\Model\Error', []),
$response->getStatusCode(),
$response->getHeaders()
];
}
$returnType = '\HubSpot\Client\Crm\Deals\Model\SimplePublicObject';
if ($returnType === '\SplFileObject') {
$content = $response->getBody(); //stream goes to serializer
} else {
$content = (string) $response->getBody();
}
return [
ObjectSerializer::deserialize($content, $returnType, []),
$response->getStatusCode(),
$response->getHeaders()
];
} catch (ApiException $e) {
switch ($e->getCode()) {
case 201:
$data = ObjectSerializer::deserialize(
$e->getResponseBody(),
'\HubSpot\Client\Crm\Deals\Model\SimplePublicObject',
$e->getResponseHeaders()
);
$e->setResponseObject($data);
break;
default:
$data = ObjectSerializer::deserialize(
$e->getResponseBody(),
'\HubSpot\Client\Crm\Deals\Model\Error',
$e->getResponseHeaders()
);
$e->setResponseObject($data);
break;
}
throw $e;
}
}
/**
* Operation createAsync
*
* Create
*
* @param \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectInput $simple_public_object_input (required)
*
* @throws \InvalidArgumentException
* @return \GuzzleHttp\Promise\PromiseInterface
*/
public function createAsync($simple_public_object_input)
{
return $this->createAsyncWithHttpInfo($simple_public_object_input)
->then(
function ($response) {
return $response[0];
}
);
}
/**
* Operation createAsyncWithHttpInfo
*
* Create
*
* @param \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectInput $simple_public_object_input (required)
*
* @throws \InvalidArgumentException
* @return \GuzzleHttp\Promise\PromiseInterface
*/
public function createAsyncWithHttpInfo($simple_public_object_input)
{
$returnType = '\HubSpot\Client\Crm\Deals\Model\SimplePublicObject';
$request = $this->createRequest($simple_public_object_input);
return $this->client
->sendAsync($request, $this->createHttpClientOption())
->then(
function ($response) use ($returnType) {
if ($returnType === '\SplFileObject') {
$content = $response->getBody(); //stream goes to serializer
} else {
$content = (string) $response->getBody();
}
return [
ObjectSerializer::deserialize($content, $returnType, []),
$response->getStatusCode(),
$response->getHeaders()
];
},
function ($exception) {
$response = $exception->getResponse();
$statusCode = $response->getStatusCode();
throw new ApiException(
sprintf(
'[%d] Error connecting to the API (%s)',
$statusCode,
$exception->getRequest()->getUri()
),
$statusCode,
$response->getHeaders(),
(string) $response->getBody()
);
}
);
}
/**
* Create request for operation 'create'
*
* @param \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectInput $simple_public_object_input (required)
*
* @throws \InvalidArgumentException
* @return \GuzzleHttp\Psr7\Request
*/
public function createRequest($simple_public_object_input)
{
// verify the required parameter 'simple_public_object_input' is set
if ($simple_public_object_input === null || (is_array($simple_public_object_input) && count($simple_public_object_input) === 0)) {
throw new \InvalidArgumentException(
'Missing the required parameter $simple_public_object_input when calling create'
);
}
$resourcePath = '/crm/v3/objects/deals';
$formParams = [];
$queryParams = [];
$headerParams = [];
$httpBody = '';
$multipart = false;
if ($multipart) {
$headers = $this->headerSelector->selectHeadersForMultipart(
['application/json', '*/*']
);
} else {
$headers = $this->headerSelector->selectHeaders(
['application/json', '*/*'],
['application/json']
);
}
// for model (json/xml)
if (isset($simple_public_object_input)) {
if ($headers['Content-Type'] === 'application/json') {
$httpBody = \GuzzleHttp\json_encode(ObjectSerializer::sanitizeForSerialization($simple_public_object_input));
} else {
$httpBody = $simple_public_object_input;
}
} elseif (count($formParams) > 0) {
if ($multipart) {
$multipartContents = [];
foreach ($formParams as $formParamName => $formParamValue) {
$formParamValueItems = is_array($formParamValue) ? $formParamValue : [$formParamValue];
foreach ($formParamValueItems as $formParamValueItem) {
$multipartContents[] = [
'name' => $formParamName,
'contents' => $formParamValueItem
];
}
}
// for HTTP post (form)
$httpBody = new MultipartStream($multipartContents);
} elseif ($headers['Content-Type'] === 'application/json') {
$httpBody = \GuzzleHttp\json_encode($formParams);
} else {
// for HTTP post (form)
$httpBody = \GuzzleHttp\Psr7\Query::build($formParams);
}
}
// this endpoint requires API key authentication
$apiKey = $this->config->getApiKeyWithPrefix('hapikey');
if ($apiKey !== null) {
$queryParams['hapikey'] = $apiKey;
}
// this endpoint requires OAuth (access token)
if ($this->config->getAccessToken() !== null) {
$headers['Authorization'] = 'Bearer ' . $this->config->getAccessToken();
}
$defaultHeaders = [];
if ($this->config->getUserAgent()) {
$defaultHeaders['User-Agent'] = $this->config->getUserAgent();
}
$headers = array_merge(
$defaultHeaders,
$headerParams,
$headers
);
$query = \GuzzleHttp\Psr7\Query::build($queryParams);
return new Request(
'POST',
$this->config->getHost() . $resourcePath . ($query ? "?{$query}" : ''),
$headers,
$httpBody
);
}
/**
* Operation getById
*
* Read
*
* @param string $deal_id deal_id (required)
* @param string[] $properties A comma separated list of the properties to be returned in the response. If any of the specified properties are not present on the requested object(s), they will be ignored. (optional)
* @param string[] $associations A comma separated list of object types to retrieve associated IDs for. If any of the specified associations do not exist, they will be ignored. (optional)
* @param bool $archived Whether to return only results that have been archived. (optional, default to false)
* @param string $id_property The name of a property whose values are unique for this object type (optional)
*
* @throws \HubSpot\Client\Crm\Deals\ApiException on non-2xx response
* @throws \InvalidArgumentException
* @return \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations|\HubSpot\Client\Crm\Deals\Model\Error
*/
public function getById($deal_id, $properties = null, $associations = null, $archived = false, $id_property = null)
{
list($response) = $this->getByIdWithHttpInfo($deal_id, $properties, $associations, $archived, $id_property);
return $response;
}
/**
* Operation getByIdWithHttpInfo
*
* Read
*
* @param string $deal_id (required)
* @param string[] $properties A comma separated list of the properties to be returned in the response. If any of the specified properties are not present on the requested object(s), they will be ignored. (optional)
* @param string[] $associations A comma separated list of object types to retrieve associated IDs for. If any of the specified associations do not exist, they will be ignored. (optional)
* @param bool $archived Whether to return only results that have been archived. (optional, default to false)
* @param string $id_property The name of a property whose values are unique for this object type (optional)
*
* @throws \HubSpot\Client\Crm\Deals\ApiException on non-2xx response
* @throws \InvalidArgumentException
* @return array of \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations|\HubSpot\Client\Crm\Deals\Model\Error, HTTP status code, HTTP response headers (array of strings)
*/
public function getByIdWithHttpInfo($deal_id, $properties = null, $associations = null, $archived = false, $id_property = null)
{
$request = $this->getByIdRequest($deal_id, $properties, $associations, $archived, $id_property);
try {
$options = $this->createHttpClientOption();
try {
$response = $this->client->send($request, $options);
} catch (RequestException $e) {
throw new ApiException(
"[{$e->getCode()}] {$e->getMessage()}",
(int) $e->getCode(),
$e->getResponse() ? $e->getResponse()->getHeaders() : null,
$e->getResponse() ? (string) $e->getResponse()->getBody() : null
);
} catch (ConnectException $e) {
throw new ApiException(
"[{$e->getCode()}] {$e->getMessage()}",
(int) $e->getCode(),
null,
null
);
}
$statusCode = $response->getStatusCode();
if ($statusCode < 200 || $statusCode > 299) {
throw new ApiException(
sprintf(
'[%d] Error connecting to the API (%s)',
$statusCode,
(string) $request->getUri()
),
$statusCode,
$response->getHeaders(),
(string) $response->getBody()
);
}
switch($statusCode) {
case 200:
if ('\HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations' === '\SplFileObject') {
$content = $response->getBody(); //stream goes to serializer
} else {
$content = (string) $response->getBody();
}
return [
ObjectSerializer::deserialize($content, '\HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations', []),
$response->getStatusCode(),
$response->getHeaders()
];
default:
if ('\HubSpot\Client\Crm\Deals\Model\Error' === '\SplFileObject') {
$content = $response->getBody(); //stream goes to serializer
} else {
$content = (string) $response->getBody();
}
return [
ObjectSerializer::deserialize($content, '\HubSpot\Client\Crm\Deals\Model\Error', []),
$response->getStatusCode(),
$response->getHeaders()
];
}
$returnType = '\HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations';
if ($returnType === '\SplFileObject') {
$content = $response->getBody(); //stream goes to serializer
} else {
$content = (string) $response->getBody();
}
return [
ObjectSerializer::deserialize($content, $returnType, []),
$response->getStatusCode(),
$response->getHeaders()
];
} catch (ApiException $e) {
switch ($e->getCode()) {
case 200:
$data = ObjectSerializer::deserialize(
$e->getResponseBody(),
'\HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations',
$e->getResponseHeaders()
);
$e->setResponseObject($data);
break;
default:
$data = ObjectSerializer::deserialize(
$e->getResponseBody(),
'\HubSpot\Client\Crm\Deals\Model\Error',
$e->getResponseHeaders()
);
$e->setResponseObject($data);
break;
}
throw $e;
}
}
/**
* Operation getByIdAsync
*
* Read
*
* @param string $deal_id (required)
* @param string[] $properties A comma separated list of the properties to be returned in the response. If any of the specified properties are not present on the requested object(s), they will be ignored. (optional)
* @param string[] $associations A comma separated list of object types to retrieve associated IDs for. If any of the specified associations do not exist, they will be ignored. (optional)
* @param bool $archived Whether to return only results that have been archived. (optional, default to false)
* @param string $id_property The name of a property whose values are unique for this object type (optional)
*
* @throws \InvalidArgumentException
* @return \GuzzleHttp\Promise\PromiseInterface
*/
public function getByIdAsync($deal_id, $properties = null, $associations = null, $archived = false, $id_property = null)
{
return $this->getByIdAsyncWithHttpInfo($deal_id, $properties, $associations, $archived, $id_property)
->then(
function ($response) {
return $response[0];
}
);
}
/**
* Operation getByIdAsyncWithHttpInfo
*
* Read
*
* @param string $deal_id (required)
* @param string[] $properties A comma separated list of the properties to be returned in the response. If any of the specified properties are not present on the requested object(s), they will be ignored. (optional)
* @param string[] $associations A comma separated list of object types to retrieve associated IDs for. If any of the specified associations do not exist, they will be ignored. (optional)
* @param bool $archived Whether to return only results that have been archived. (optional, default to false)
* @param string $id_property The name of a property whose values are unique for this object type (optional)
*
* @throws \InvalidArgumentException
* @return \GuzzleHttp\Promise\PromiseInterface
*/
public function getByIdAsyncWithHttpInfo($deal_id, $properties = null, $associations = null, $archived = false, $id_property = null)
{
$returnType = '\HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations';
$request = $this->getByIdRequest($deal_id, $properties, $associations, $archived, $id_property);
return $this->client
->sendAsync($request, $this->createHttpClientOption())
->then(
function ($response) use ($returnType) {
if ($returnType === '\SplFileObject') {
$content = $response->getBody(); //stream goes to serializer
} else {
$content = (string) $response->getBody();
}
return [
ObjectSerializer::deserialize($content, $returnType, []),
$response->getStatusCode(),
$response->getHeaders()
];
},
function ($exception) {
$response = $exception->getResponse();
$statusCode = $response->getStatusCode();
throw new ApiException(
sprintf(
'[%d] Error connecting to the API (%s)',
$statusCode,
$exception->getRequest()->getUri()
),
$statusCode,
$response->getHeaders(),
(string) $response->getBody()
);
}
);
}
/**
* Create request for operation 'getById'
*
* @param string $deal_id (required)
* @param string[] $properties A comma separated list of the properties to be returned in the response. If any of the specified properties are not present on the requested object(s), they will be ignored. (optional)
* @param string[] $associations A comma separated list of object types to retrieve associated IDs for. If any of the specified associations do not exist, they will be ignored. (optional)
* @param bool $archived Whether to return only results that have been archived. (optional, default to false)
* @param string $id_property The name of a property whose values are unique for this object type (optional)
*
* @throws \InvalidArgumentException
* @return \GuzzleHttp\Psr7\Request
*/
public function getByIdRequest($deal_id, $properties = null, $associations = null, $archived = false, $id_property = null)
{
// verify the required parameter 'deal_id' is set
if ($deal_id === null || (is_array($deal_id) && count($deal_id) === 0)) {
throw new \InvalidArgumentException(
'Missing the required parameter $deal_id when calling getById'
);
}
$resourcePath = '/crm/v3/objects/deals/{dealId}';
$formParams = [];
$queryParams = [];
$headerParams = [];
$httpBody = '';
$multipart = false;
// query params
if ($properties !== null) {
if('form' === 'form' && is_array($properties)) {
foreach($properties as $key => $value) {
$queryParams[$key] = $value;
}
}
else {
$queryParams['properties'] = $properties;
}
}
// query params
if ($associations !== null) {
if('form' === 'form' && is_array($associations)) {
foreach($associations as $key => $value) {
$queryParams[$key] = $value;
}
}
else {
$queryParams['associations'] = $associations;
}
}
// query params
if ($archived !== null) {
if('form' === 'form' && is_array($archived)) {
foreach($archived as $key => $value) {
$queryParams[$key] = $value;
}
}
else {
$queryParams['archived'] = $archived;
}
}
// query params
if ($id_property !== null) {
if('form' === 'form' && is_array($id_property)) {
foreach($id_property as $key => $value) {
$queryParams[$key] = $value;
}
}
else {
$queryParams['idProperty'] = $id_property;
}
}
// path params
if ($deal_id !== null) {
$resourcePath = str_replace(
'{' . 'dealId' . '}',
ObjectSerializer::toPathValue($deal_id),
$resourcePath
);
}
if ($multipart) {
$headers = $this->headerSelector->selectHeadersForMultipart(
['application/json', '*/*']
);
} else {
$headers = $this->headerSelector->selectHeaders(
['application/json', '*/*'],
[]
);
}
// for model (json/xml)
if (count($formParams) > 0) {
if ($multipart) {
$multipartContents = [];
foreach ($formParams as $formParamName => $formParamValue) {
$formParamValueItems = is_array($formParamValue) ? $formParamValue : [$formParamValue];
foreach ($formParamValueItems as $formParamValueItem) {
$multipartContents[] = [
'name' => $formParamName,
'contents' => $formParamValueItem
];
}
}
// for HTTP post (form)
$httpBody = new MultipartStream($multipartContents);
} elseif ($headers['Content-Type'] === 'application/json') {
$httpBody = \GuzzleHttp\json_encode($formParams);
} else {
// for HTTP post (form)
$httpBody = \GuzzleHttp\Psr7\Query::build($formParams);
}
}
// this endpoint requires API key authentication
$apiKey = $this->config->getApiKeyWithPrefix('hapikey');
if ($apiKey !== null) {
$queryParams['hapikey'] = $apiKey;
}
// this endpoint requires OAuth (access token)
if ($this->config->getAccessToken() !== null) {
$headers['Authorization'] = 'Bearer ' . $this->config->getAccessToken();
}
$defaultHeaders = [];
if ($this->config->getUserAgent()) {
$defaultHeaders['User-Agent'] = $this->config->getUserAgent();
}
$headers = array_merge(
$defaultHeaders,
$headerParams,
$headers
);
$query = \GuzzleHttp\Psr7\Query::build($queryParams);
return new Request(
'GET',
$this->config->getHost() . $resourcePath . ($query ? "?{$query}" : ''),
$headers,
$httpBody
);
}
/**
* Operation getPage
*
* List
*
* @param int $limit The maximum number of results to display per page. (optional, default to 10)
* @param string $after The paging cursor token of the last successfully read resource will be returned as the `paging.next.after` JSON property of a paged response containing more results. (optional)
* @param string[] $properties A comma separated list of the properties to be returned in the response. If any of the specified properties are not present on the requested object(s), they will be ignored. (optional)
* @param string[] $associations A comma separated list of object types to retrieve associated IDs for. If any of the specified associations do not exist, they will be ignored. (optional)
* @param bool $archived Whether to return only results that have been archived. (optional, default to false)
*
* @throws \HubSpot\Client\Crm\Deals\ApiException on non-2xx response
* @throws \InvalidArgumentException
* @return \HubSpot\Client\Crm\Deals\Model\CollectionResponseSimplePublicObjectWithAssociationsForwardPaging|\HubSpot\Client\Crm\Deals\Model\Error
*/
public function getPage($limit = 10, $after = null, $properties = null, $associations = null, $archived = false)
{
list($response) = $this->getPageWithHttpInfo($limit, $after, $properties, $associations, $archived);
return $response;
}
/**
* Operation getPageWithHttpInfo
*
* List
*
* @param int $limit The maximum number of results to display per page. (optional, default to 10)
* @param string $after The paging cursor token of the last successfully read resource will be returned as the `paging.next.after` JSON property of a paged response containing more results. (optional)
* @param string[] $properties A comma separated list of the properties to be returned in the response. If any of the specified properties are not present on the requested object(s), they will be ignored. (optional)
* @param string[] $associations A comma separated list of object types to retrieve associated IDs for. If any of the specified associations do not exist, they will be ignored. (optional)
* @param bool $archived Whether to return only results that have been archived. (optional, default to false)
*
* @throws \HubSpot\Client\Crm\Deals\ApiException on non-2xx response
* @throws \InvalidArgumentException
* @return array of \HubSpot\Client\Crm\Deals\Model\CollectionResponseSimplePublicObjectWithAssociationsForwardPaging|\HubSpot\Client\Crm\Deals\Model\Error, HTTP status code, HTTP response headers (array of strings)
*/
public function getPageWithHttpInfo($limit = 10, $after = null, $properties = null, $associations = null, $archived = false)
{
$request = $this->getPageRequest($limit, $after, $properties, $associations, $archived);
try {
$options = $this->createHttpClientOption();
try {
$response = $this->client->send($request, $options);
} catch (RequestException $e) {
throw new ApiException(
"[{$e->getCode()}] {$e->getMessage()}",
(int) $e->getCode(),
$e->getResponse() ? $e->getResponse()->getHeaders() : null,
$e->getResponse() ? (string) $e->getResponse()->getBody() : null
);
} catch (ConnectException $e) {
throw new ApiException(
"[{$e->getCode()}] {$e->getMessage()}",
(int) $e->getCode(),
null,
null
);
}
$statusCode = $response->getStatusCode();
if ($statusCode < 200 || $statusCode > 299) {
throw new ApiException(
sprintf(
'[%d] Error connecting to the API (%s)',
$statusCode,
(string) $request->getUri()
),
$statusCode,
$response->getHeaders(),
(string) $response->getBody()
);
}
switch($statusCode) {
case 200:
if ('\HubSpot\Client\Crm\Deals\Model\CollectionResponseSimplePublicObjectWithAssociationsForwardPaging' === '\SplFileObject') {
$content = $response->getBody(); //stream goes to serializer
} else {
$content = (string) $response->getBody();
}
return [
ObjectSerializer::deserialize($content, '\HubSpot\Client\Crm\Deals\Model\CollectionResponseSimplePublicObjectWithAssociationsForwardPaging', []),
$response->getStatusCode(),
$response->getHeaders()
];
default:
if ('\HubSpot\Client\Crm\Deals\Model\Error' === '\SplFileObject') {
$content = $response->getBody(); //stream goes to serializer
} else {
$content = (string) $response->getBody();
}
return [
ObjectSerializer::deserialize($content, '\HubSpot\Client\Crm\Deals\Model\Error', []),
$response->getStatusCode(),
$response->getHeaders()
];
}
$returnType = '\HubSpot\Client\Crm\Deals\Model\CollectionResponseSimplePublicObjectWithAssociationsForwardPaging';
if ($returnType === '\SplFileObject') {
$content = $response->getBody(); //stream goes to serializer
} else {
$content = (string) $response->getBody();
}
return [
ObjectSerializer::deserialize($content, $returnType, []),
$response->getStatusCode(),
$response->getHeaders()
];
} catch (ApiException $e) {
switch ($e->getCode()) {
case 200:
$data = ObjectSerializer::deserialize(
$e->getResponseBody(),
'\HubSpot\Client\Crm\Deals\Model\CollectionResponseSimplePublicObjectWithAssociationsForwardPaging',
$e->getResponseHeaders()
);
$e->setResponseObject($data);
break;
default:
$data = ObjectSerializer::deserialize(
$e->getResponseBody(),
'\HubSpot\Client\Crm\Deals\Model\Error',
$e->getResponseHeaders()
);
$e->setResponseObject($data);
break;
}
throw $e;
}
}
/**
* Operation getPageAsync
*
* List
*
* @param int $limit The maximum number of results to display per page. (optional, default to 10)
* @param string $after The paging cursor token of the last successfully read resource will be returned as the `paging.next.after` JSON property of a paged response containing more results. (optional)
* @param string[] $properties A comma separated list of the properties to be returned in the response. If any of the specified properties are not present on the requested object(s), they will be ignored. (optional)
* @param string[] $associations A comma separated list of object types to retrieve associated IDs for. If any of the specified associations do not exist, they will be ignored. (optional)
* @param bool $archived Whether to return only results that have been archived. (optional, default to false)
*
* @throws \InvalidArgumentException
* @return \GuzzleHttp\Promise\PromiseInterface
*/
public function getPageAsync($limit = 10, $after = null, $properties = null, $associations = null, $archived = false)
{
return $this->getPageAsyncWithHttpInfo($limit, $after, $properties, $associations, $archived)
->then(
function ($response) {
return $response[0];
}
);
}
/**
* Operation getPageAsyncWithHttpInfo
*
* List
*
* @param int $limit The maximum number of results to display per page. (optional, default to 10)
* @param string $after The paging cursor token of the last successfully read resource will be returned as the `paging.next.after` JSON property of a paged response containing more results. (optional)
* @param string[] $properties A comma separated list of the properties to be returned in the response. If any of the specified properties are not present on the requested object(s), they will be ignored. (optional)
* @param string[] $associations A comma separated list of object types to retrieve associated IDs for. If any of the specified associations do not exist, they will be ignored. (optional)
* @param bool $archived Whether to return only results that have been archived. (optional, default to false)
*
* @throws \InvalidArgumentException
* @return \GuzzleHttp\Promise\PromiseInterface
*/
public function getPageAsyncWithHttpInfo($limit = 10, $after = null, $properties = null, $associations = null, $archived = false)
{
$returnType = '\HubSpot\Client\Crm\Deals\Model\CollectionResponseSimplePublicObjectWithAssociationsForwardPaging';
$request = $this->getPageRequest($limit, $after, $properties, $associations, $archived);
return $this->client
->sendAsync($request, $this->createHttpClientOption())
->then(
function ($response) use ($returnType) {
if ($returnType === '\SplFileObject') {
$content = $response->getBody(); //stream goes to serializer
} else {
$content = (string) $response->getBody();
}
return [
ObjectSerializer::deserialize($content, $returnType, []),
$response->getStatusCode(),
$response->getHeaders()
];
},
function ($exception) {
$response = $exception->getResponse();
$statusCode = $response->getStatusCode();
throw new ApiException(
sprintf(
'[%d] Error connecting to the API (%s)',
$statusCode,
$exception->getRequest()->getUri()
),
$statusCode,
$response->getHeaders(),
(string) $response->getBody()
);
}
);
}
/**
* Create request for operation 'getPage'
*
* @param int $limit The maximum number of results to display per page. (optional, default to 10)
* @param string $after The paging cursor token of the last successfully read resource will be returned as the `paging.next.after` JSON property of a paged response containing more results. (optional)
* @param string[] $properties A comma separated list of the properties to be returned in the response. If any of the specified properties are not present on the requested object(s), they will be ignored. (optional)
* @param string[] $associations A comma separated list of object types to retrieve associated IDs for. If any of the specified associations do not exist, they will be ignored. (optional)
* @param bool $archived Whether to return only results that have been archived. (optional, default to false)
*
* @throws \InvalidArgumentException
* @return \GuzzleHttp\Psr7\Request
*/
public function getPageRequest($limit = 10, $after = null, $properties = null, $associations = null, $archived = false)
{
$resourcePath = '...
|
2887
|
NULL
|
NULL
|
NULL
|
|
2887
|
116
|
8
|
2026-05-07T11:46:56.751499+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778154416751_m2.jpg...
|
PhpStorm
|
faVsco.js – ~/jiminny/app/vendor/hubspot/api-clien faVsco.js – ~/jiminny/app/vendor/hubspot/api-client/codegen/Crm/Deals/Api/BasicApi.php...
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
Editor for custom.log
Code changed:
Hide
Sync Changes
Hide This Notification
Reader Mode
Analyzing…
<?php
/**
* BasicApi
* PHP version 7.3
*
* @category Class
* @package HubSpot\Client\Crm\Deals
* @author OpenAPI Generator team
* @link [URL_WITH_CREDENTIALS] Class
* @package HubSpot\Client\Crm\Deals
* @author OpenAPI Generator team
* @link [URL_WITH_CREDENTIALS] int $hostIndex Host index (required)
*/
public function setHostIndex($hostIndex): void
{
$this->hostIndex = $hostIndex;
}
/**
* Get the host index
*
* @return int Host index
*/
public function getHostIndex()
{
return $this->hostIndex;
}
/**
* @return Configuration
*/
public function getConfig()
{
return $this->config;
}
/**
* Operation archive
*
* Archive
*
* @param string $deal_id deal_id (required)
*
* @throws \HubSpot\Client\Crm\Deals\ApiException on non-2xx response
* @throws \InvalidArgumentException
* @return void
*/
public function archive($deal_id)
{
$this->archiveWithHttpInfo($deal_id);
}
/**
* Operation archiveWithHttpInfo
*
* Archive
*
* @param string $deal_id (required)
*
* @throws \HubSpot\Client\Crm\Deals\ApiException on non-2xx response
* @throws \InvalidArgumentException
* @return array of null, HTTP status code, HTTP response headers (array of strings)
*/
public function archiveWithHttpInfo($deal_id)
{
$request = $this->archiveRequest($deal_id);
try {
$options = $this->createHttpClientOption();
try {
$response = $this->client->send($request, $options);
} catch (RequestException $e) {
throw new ApiException(
"[{$e->getCode()}] {$e->getMessage()}",
(int) $e->getCode(),
$e->getResponse() ? $e->getResponse()->getHeaders() : null,
$e->getResponse() ? (string) $e->getResponse()->getBody() : null
);
} catch (ConnectException $e) {
throw new ApiException(
"[{$e->getCode()}] {$e->getMessage()}",
(int) $e->getCode(),
null,
null
);
}
$statusCode = $response->getStatusCode();
if ($statusCode < 200 || $statusCode > 299) {
throw new ApiException(
sprintf(
'[%d] Error connecting to the API (%s)',
$statusCode,
(string) $request->getUri()
),
$statusCode,
$response->getHeaders(),
(string) $response->getBody()
);
}
return [null, $statusCode, $response->getHeaders()];
} catch (ApiException $e) {
switch ($e->getCode()) {
default:
$data = ObjectSerializer::deserialize(
$e->getResponseBody(),
'\HubSpot\Client\Crm\Deals\Model\Error',
$e->getResponseHeaders()
);
$e->setResponseObject($data);
break;
}
throw $e;
}
}
/**
* Operation archiveAsync
*
* Archive
*
* @param string $deal_id (required)
*
* @throws \InvalidArgumentException
* @return \GuzzleHttp\Promise\PromiseInterface
*/
public function archiveAsync($deal_id)
{
return $this->archiveAsyncWithHttpInfo($deal_id)
->then(
function ($response) {
return $response[0];
}
);
}
/**
* Operation archiveAsyncWithHttpInfo
*
* Archive
*
* @param string $deal_id (required)
*
* @throws \InvalidArgumentException
* @return \GuzzleHttp\Promise\PromiseInterface
*/
public function archiveAsyncWithHttpInfo($deal_id)
{
$returnType = '';
$request = $this->archiveRequest($deal_id);
return $this->client
->sendAsync($request, $this->createHttpClientOption())
->then(
function ($response) use ($returnType) {
return [null, $response->getStatusCode(), $response->getHeaders()];
},
function ($exception) {
$response = $exception->getResponse();
$statusCode = $response->getStatusCode();
throw new ApiException(
sprintf(
'[%d] Error connecting to the API (%s)',
$statusCode,
$exception->getRequest()->getUri()
),
$statusCode,
$response->getHeaders(),
(string) $response->getBody()
);
}
);
}
/**
* Create request for operation 'archive'
*
* @param string $deal_id (required)
*
* @throws \InvalidArgumentException
* @return \GuzzleHttp\Psr7\Request
*/
public function archiveRequest($deal_id)
{
// verify the required parameter 'deal_id' is set
if ($deal_id === null || (is_array($deal_id) && count($deal_id) === 0)) {
throw new \InvalidArgumentException(
'Missing the required parameter $deal_id when calling archive'
);
}
$resourcePath = '/crm/v3/objects/deals/{dealId}';
$formParams = [];
$queryParams = [];
$headerParams = [];
$httpBody = '';
$multipart = false;
// path params
if ($deal_id !== null) {
$resourcePath = str_replace(
'{' . 'dealId' . '}',
ObjectSerializer::toPathValue($deal_id),
$resourcePath
);
}
if ($multipart) {
$headers = $this->headerSelector->selectHeadersForMultipart(
['*/*']
);
} else {
$headers = $this->headerSelector->selectHeaders(
['*/*'],
[]
);
}
// for model (json/xml)
if (count($formParams) > 0) {
if ($multipart) {
$multipartContents = [];
foreach ($formParams as $formParamName => $formParamValue) {
$formParamValueItems = is_array($formParamValue) ? $formParamValue : [$formParamValue];
foreach ($formParamValueItems as $formParamValueItem) {
$multipartContents[] = [
'name' => $formParamName,
'contents' => $formParamValueItem
];
}
}
// for HTTP post (form)
$httpBody = new MultipartStream($multipartContents);
} elseif ($headers['Content-Type'] === 'application/json') {
$httpBody = \GuzzleHttp\json_encode($formParams);
} else {
// for HTTP post (form)
$httpBody = \GuzzleHttp\Psr7\Query::build($formParams);
}
}
// this endpoint requires API key authentication
$apiKey = $this->config->getApiKeyWithPrefix('hapikey');
if ($apiKey !== null) {
$queryParams['hapikey'] = $apiKey;
}
// this endpoint requires OAuth (access token)
if ($this->config->getAccessToken() !== null) {
$headers['Authorization'] = 'Bearer ' . $this->config->getAccessToken();
}
$defaultHeaders = [];
if ($this->config->getUserAgent()) {
$defaultHeaders['User-Agent'] = $this->config->getUserAgent();
}
$headers = array_merge(
$defaultHeaders,
$headerParams,
$headers
);
$query = \GuzzleHttp\Psr7\Query::build($queryParams);
return new Request(
'DELETE',
$this->config->getHost() . $resourcePath . ($query ? "?{$query}" : ''),
$headers,
$httpBody
);
}
/**
* Operation create
*
* Create
*
* @param \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectInput $simple_public_object_input simple_public_object_input (required)
*
* @throws \HubSpot\Client\Crm\Deals\ApiException on non-2xx response
* @throws \InvalidArgumentException
* @return \HubSpot\Client\Crm\Deals\Model\SimplePublicObject|\HubSpot\Client\Crm\Deals\Model\Error
*/
public function create($simple_public_object_input)
{
list($response) = $this->createWithHttpInfo($simple_public_object_input);
return $response;
}
/**
* Operation createWithHttpInfo
*
* Create
*
* @param \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectInput $simple_public_object_input (required)
*
* @throws \HubSpot\Client\Crm\Deals\ApiException on non-2xx response
* @throws \InvalidArgumentException
* @return array of \HubSpot\Client\Crm\Deals\Model\SimplePublicObject|\HubSpot\Client\Crm\Deals\Model\Error, HTTP status code, HTTP response headers (array of strings)
*/
public function createWithHttpInfo($simple_public_object_input)
{
$request = $this->createRequest($simple_public_object_input);
try {
$options = $this->createHttpClientOption();
try {
$response = $this->client->send($request, $options);
} catch (RequestException $e) {
throw new ApiException(
"[{$e->getCode()}] {$e->getMessage()}",
(int) $e->getCode(),
$e->getResponse() ? $e->getResponse()->getHeaders() : null,
$e->getResponse() ? (string) $e->getResponse()->getBody() : null
);
} catch (ConnectException $e) {
throw new ApiException(
"[{$e->getCode()}] {$e->getMessage()}",
(int) $e->getCode(),
null,
null
);
}
$statusCode = $response->getStatusCode();
if ($statusCode < 200 || $statusCode > 299) {
throw new ApiException(
sprintf(
'[%d] Error connecting to the API (%s)',
$statusCode,
(string) $request->getUri()
),
$statusCode,
$response->getHeaders(),
(string) $response->getBody()
);
}
switch($statusCode) {
case 201:
if ('\HubSpot\Client\Crm\Deals\Model\SimplePublicObject' === '\SplFileObject') {
$content = $response->getBody(); //stream goes to serializer
} else {
$content = (string) $response->getBody();
}
return [
ObjectSerializer::deserialize($content, '\HubSpot\Client\Crm\Deals\Model\SimplePublicObject', []),
$response->getStatusCode(),
$response->getHeaders()
];
default:
if ('\HubSpot\Client\Crm\Deals\Model\Error' === '\SplFileObject') {
$content = $response->getBody(); //stream goes to serializer
} else {
$content = (string) $response->getBody();
}
return [
ObjectSerializer::deserialize($content, '\HubSpot\Client\Crm\Deals\Model\Error', []),
$response->getStatusCode(),
$response->getHeaders()
];
}
$returnType = '\HubSpot\Client\Crm\Deals\Model\SimplePublicObject';
if ($returnType === '\SplFileObject') {
$content = $response->getBody(); //stream goes to serializer
} else {
$content = (string) $response->getBody();
}
return [
ObjectSerializer::deserialize($content, $returnType, []),
$response->getStatusCode(),
$response->getHeaders()
];
} catch (ApiException $e) {
switch ($e->getCode()) {
case 201:
$data = ObjectSerializer::deserialize(
$e->getResponseBody(),
'\HubSpot\Client\Crm\Deals\Model\SimplePublicObject',
$e->getResponseHeaders()
);
$e->setResponseObject($data);
break;
default:
$data = ObjectSerializer::deserialize(
$e->getResponseBody(),
'\HubSpot\Client\Crm\Deals\Model\Error',
$e->getResponseHeaders()
);
$e->setResponseObject($data);
break;
}
throw $e;
}
}
/**
* Operation createAsync
*
* Create
*
* @param \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectInput $simple_public_object_input (required)
*
* @throws \InvalidArgumentException
* @return \GuzzleHttp\Promise\PromiseInterface
*/
public function createAsync($simple_public_object_input)
{
return $this->createAsyncWithHttpInfo($simple_public_object_input)
->then(
function ($response) {
return $response[0];
}
);
}
/**
* Operation createAsyncWithHttpInfo
*
* Create
*
* @param \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectInput $simple_public_object_input (required)
*
* @throws \InvalidArgumentException
* @return \GuzzleHttp\Promise\PromiseInterface
*/
public function createAsyncWithHttpInfo($simple_public_object_input)
{
$returnType = '\HubSpot\Client\Crm\Deals\Model\SimplePublicObject';
$request = $this->createRequest($simple_public_object_input);
return $this->client
->sendAsync($request, $this->createHttpClientOption())
->then(
function ($response) use ($returnType) {
if ($returnType === '\SplFileObject') {
$content = $response->getBody(); //stream goes to serializer
} else {
$content = (string) $response->getBody();
}
return [
ObjectSerializer::deserialize($content, $returnType, []),
$response->getStatusCode(),
$response->getHeaders()
];
},
function ($exception) {
$response = $exception->getResponse();
$statusCode = $response->getStatusCode();
throw new ApiException(
sprintf(
'[%d] Error connecting to the API (%s)',
$statusCode,
$exception->getRequest()->getUri()
),
$statusCode,
$response->getHeaders(),
(string) $response->getBody()
);
}
);
}
/**
* Create request for operation 'create'
*
* @param \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectInput $simple_public_object_input (required)
*
* @throws \InvalidArgumentException
* @return \GuzzleHttp\Psr7\Request
*/
public function createRequest($simple_public_object_input)
{
// verify the required parameter 'simple_public_object_input' is set
if ($simple_public_object_input === null || (is_array($simple_public_object_input) && count($simple_public_object_input) === 0)) {
throw new \InvalidArgumentException(
'Missing the required parameter $simple_public_object_input when calling create'
);
}
$resourcePath = '/crm/v3/objects/deals';
$formParams = [];
$queryParams = [];
$headerParams = [];
$httpBody = '';
$multipart = false;
if ($multipart) {
$headers = $this->headerSelector->selectHeadersForMultipart(
['application/json', '*/*']
);
} else {
$headers = $this->headerSelector->selectHeaders(
['application/json', '*/*'],
['application/json']
);
}
// for model (json/xml)
if (isset($simple_public_object_input)) {
if ($headers['Content-Type'] === 'application/json') {
$httpBody = \GuzzleHttp\json_encode(ObjectSerializer::sanitizeForSerialization($simple_public_object_input));
} else {
$httpBody = $simple_public_object_input;
}
} elseif (count($formParams) > 0) {
if ($multipart) {
$multipartContents = [];
foreach ($formParams as $formParamName => $formParamValue) {
$formParamValueItems = is_array($formParamValue) ? $formParamValue : [$formParamValue];
foreach ($formParamValueItems as $formParamValueItem) {
$multipartContents[] = [
'name' => $formParamName,
'contents' => $formParamValueItem
];
}
}
// for HTTP post (form)
$httpBody = new MultipartStream($multipartContents);
} elseif ($headers['Content-Type'] === 'application/json') {
$httpBody = \GuzzleHttp\json_encode($formParams);
} else {
// for HTTP post (form)
$httpBody = \GuzzleHttp\Psr7\Query::build($formParams);
}
}
// this endpoint requires API key authentication
$apiKey = $this->config->getApiKeyWithPrefix('hapikey');
if ($apiKey !== null) {
$queryParams['hapikey'] = $apiKey;
}
// this endpoint requires OAuth (access token)
if ($this->config->getAccessToken() !== null) {
$headers['Authorization'] = 'Bearer ' . $this->config->getAccessToken();
}
$defaultHeaders = [];
if ($this->config->getUserAgent()) {
$defaultHeaders['User-Agent'] = $this->config->getUserAgent();
}
$headers = array_merge(
$defaultHeaders,
$headerParams,
$headers
);
$query = \GuzzleHttp\Psr7\Query::build($queryParams);
return new Request(
'POST',
$this->config->getHost() . $resourcePath . ($query ? "?{$query}" : ''),
$headers,
$httpBody
);
}
/**
* Operation getById
*
* Read
*
* @param string $deal_id deal_id (required)
* @param string[] $properties A comma separated list of the properties to be returned in the response. If any of the specified properties are not present on the requested object(s), they will be ignored. (optional)
* @param string[] $associations A comma separated list of object types to retrieve associated IDs for. If any of the specified associations do not exist, they will be ignored. (optional)
* @param bool $archived Whether to return only results that have been archived. (optional, default to false)
* @param string $id_property The name of a property whose values are unique for this object type (optional)
*
* @throws \HubSpot\Client\Crm\Deals\ApiException on non-2xx response
* @throws \InvalidArgumentException
* @return \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations|\HubSpot\Client\Crm\Deals\Model\Error
*/
public function getById($deal_id, $properties = null, $associations = null, $archived = false, $id_property = null)
{
list($response) = $this->getByIdWithHttpInfo($deal_id, $properties, $associations, $archived, $id_property);
return $response;
}
/**
* Operation getByIdWithHttpInfo
*
* Read
*
* @param string $deal_id (required)
* @param string[] $properties A comma separated list of the properties to be returned in the response. If any of the specified properties are not present on the requested object(s), they will be ignored. (optional)
* @param string[] $associations A comma separated list of object types to retrieve associated IDs for. If any of the specified associations do not exist, they will be ignored. (optional)
* @param bool $archived Whether to return only results that have been archived. (optional, default to false)
* @param string $id_property The name of a property whose values are unique for this object type (optional)
*
* @throws \HubSpot\Client\Crm\Deals\ApiException on non-2xx response
* @throws \InvalidArgumentException
* @return array of \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations|\HubSpot\Client\Crm\Deals\Model\Error, HTTP status code, HTTP response headers (array of strings)
*/
public function getByIdWithHttpInfo($deal_id, $properties = null, $associations = null, $archived = false, $id_property = null)
{
$request = $this->getByIdRequest($deal_id, $properties, $associations, $archived, $id_property);
try {
$options = $this->createHttpClientOption();
try {
$response = $this->client->send($request, $options);
} catch (RequestException $e) {
throw new ApiException(
"[{$e->getCode()}] {$e->getMessage()}",
(int) $e->getCode(),
$e->getResponse() ? $e->getResponse()->getHeaders() : null,
$e->getResponse() ? (string) $e->getResponse()->getBody() : null
);
} catch (ConnectException $e) {
throw new ApiException(
"[{$e->getCode()}] {$e->getMessage()}",
(int) $e->getCode(),
null,
null
);
}
$statusCode = $response->getStatusCode();
if ($statusCode < 200 || $statusCode > 299) {
throw new ApiException(
sprintf(
'[%d] Error connecting to the API (%s)',
$statusCode,
(string) $request->getUri()
),
$statusCode,
$response->getHeaders(),
(string) $response->getBody()
);
}
switch($statusCode) {
case 200:
if ('\HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations' === '\SplFileObject') {
$content = $response->getBody(); //stream goes to serializer
} else {
$content = (string) $response->getBody();
}
return [
ObjectSerializer::deserialize($content, '\HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations', []),
$response->getStatusCode(),
$response->getHeaders()
];
default:
if ('\HubSpot\Client\Crm\Deals\Model\Error' === '\SplFileObject') {
$content = $response->getBody(); //stream goes to serializer
} else {
$content = (string) $response->getBody();
}
return [
ObjectSerializer::deserialize($content, '\HubSpot\Client\Crm\Deals\Model\Error', []),
$response->getStatusCode(),
$response->getHeaders()
];
}
$returnType = '\HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations';
if ($returnType === '\SplFileObject') {
$content = $response->getBody(); //stream goes to serializer
} else {
$content = (string) $response->getBody();
}
return [
ObjectSerializer::deserialize($content, $returnType, []),
$response->getStatusCode(),
$response->getHeaders()
];
} catch (ApiException $e) {
switch ($e->getCode()) {
case 200:
$data = ObjectSerializer::deserialize(
$e->getResponseBody(),
'\HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations',
$e->getResponseHeaders()
);
$e->setResponseObject($data);
break;
default:
$data = ObjectSerializer::deserialize(
$e->getResponseBody(),
'\HubSpot\Client\Crm\Deals\Model\Error',
$e->getResponseHeaders()
);
$e->setResponseObject($data);
break;
}
throw $e;
}
}
/**
* Operation getByIdAsync
*
* Read
*
* @param string $deal_id (required)
* @param string[] $properties A comma separated list of the properties to be returned in the response. If any of the specified properties are not present on the requested object(s), they will be ignored. (optional)
* @param string[] $associations A comma separated list of object types to retrieve associated IDs for. If any of the specified associations do not exist, they will be ignored. (optional)
* @param bool $archived Whether to return only results that have been archived. (optional, default to false)
* @param string $id_property The name of a property whose values are unique for this object type (optional)
*
* @throws \InvalidArgumentException
* @return \GuzzleHttp\Promise\PromiseInterface
*/
public function getByIdAsync($deal_id, $properties = null, $associations = null, $archived = false, $id_property = null)
{
return $this->getByIdAsyncWithHttpInfo($deal_id, $properties, $associations, $archived, $id_property)
->then(
function ($response) {
return $response[0];
}
);
}
/**
* Operation getByIdAsyncWithHttpInfo
*
* Read
*
* @param string $deal_id (required)
* @param string[] $properties A comma separated list of the properties to be returned in the response. If any of the specified properties are not present on the requested object(s), they will be ignored. (optional)
* @param string[] $associations A comma separated list of object types to retrieve associated IDs for. If any of the specified associations do not exist, they will be ignored. (optional)
* @param bool $archived Whether to return only results that have been archived. (optional, default to false)
* @param string $id_property The name of a property whose values are unique for this object type (optional)
*
* @throws \InvalidArgumentException
* @return \GuzzleHttp\Promise\PromiseInterface
*/
public function getByIdAsyncWithHttpInfo($deal_id, $properties = null, $associations = null, $archived = false, $id_property = null)
{
$returnType = '\HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations';
$request = $this->getByIdRequest($deal_id, $properties, $associations, $archived, $id_property);
return $this->client
->sendAsync($request, $this->createHttpClientOption())
->then(
function ($response) use ($returnType) {
if ($returnType === '\SplFileObject') {
$content = $response->getBody(); //stream goes to serializer
} else {
$content = (string) $response->getBody();
}
return [
ObjectSerializer::deserialize($content, $returnType, []),
$response->getStatusCode(),
$response->getHeaders()
];
},
function ($exception) {
$response = $exception->getResponse();
$statusCode = $response->getStatusCode();
throw new ApiException(
sprintf(
'[%d] Error connecting to the API (%s)',
$statusCode,
$exception->getRequest()->getUri()
),
$statusCode,
$response->getHeaders(),
(string) $response->getBody()
);
}
);
}
/**
* Create request for operation 'getById'
*
* @param string $deal_id (required)
* @param string[] $properties A comma separated list of the properties to be returned in the response. If any of the specified properties are not present on the requested object(s), they will be ignored. (optional)
* @param string[] $associations A comma separated list of object types to retrieve associated IDs for. If any of the specified associations do not exist, they will be ignored. (optional)
* @param bool $archived Whether to return only results that have been archived. (optional, default to false)
* @param string $id_property The name of a property whose values are unique for this object type (optional)
*
* @throws \InvalidArgumentException
* @return \GuzzleHttp\Psr7\Request
*/
public function getByIdRequest($deal_id, $properties = null, $associations = null, $archived = false, $id_property = null)
{
// verify the required parameter 'deal_id' is set
if ($deal_id === null || (is_array($deal_id) && count($deal_id) === 0)) {
throw new \InvalidArgumentException(
'Missing the required parameter $deal_id when calling getById'
);
}
$resourcePath = '/crm/v3/objects/deals/{dealId}';
$formParams = [];
$queryParams = [];
$headerParams = [];
$httpBody = '';
$multipart = false;
// query params
if ($properties !== null) {
if('form' === 'form' && is_array($properties)) {
foreach($properties as $key => $value) {
$queryParams[$key] = $value;
}
}
else {
$queryParams['properties'] = $properties;
}
}
// query params
if ($associations !== null) {
if('form' === 'form' && is_array($associations)) {
foreach($associations as $key => $value) {
$queryParams[$key] = $value;
}
}
else {
$queryParams['associations'] = $associations;
}
}
// query params
if ($archived !== null) {
if('form' === 'form' && is_array($archived)) {
foreach($archived as $key => $value) {
$queryParams[$key] = $value;
}
}
else {
$queryParams['archived'] = $archived;
}
}
// query params
if ($id_property !== null) {
if('form' === 'form' && is_array($id_property)) {
foreach($id_property as $key => $value) {
$queryParams[$key] = $value;
}
}
else {
$queryParams['idProperty'] = $id_property;
}
}
// path params
if ($deal_id !== null) {
$resourcePath = str_replace(
'{' . 'dealId' . '}',
ObjectSerializer::toPathValue($deal_id),
$resourcePath
);
}
if ($multipart) {
$headers = $this->headerSelector->selectHeadersForMultipart(
['application/json', '*/*']
);
} else {
$headers = $this->headerSelector->selectHeaders(
['application/json', '*/*'],
[]
);
}
// for model (json/xml)
if (count($formParams) > 0) {
if ($multipart) {
$multipartContents = [];
foreach ($formParams as $formParamName => $formParamValue) {
$formParamValueItems = is_array($formParamValue) ? $formParamValue : [$formParamValue];
foreach ($formParamValueItems as $formParamValueItem) {
$multipartContents[] = [
'name' => $formParamName,
'contents' => $formParamValueItem
];
}
}
// for HTTP post (form)
$httpBody = new MultipartStream($multipartContents);
} elseif ($headers['Content-Type'] === 'application/json') {
$httpBody = \GuzzleHttp\json_encode($formParams);
} else {
// for HTTP post (form)
$httpBody = \GuzzleHttp\Psr7\Query::build($formParams);
}
}
// this endpoint requires API key authentication
$apiKey = $this->config->getApiKeyWithPrefix('hapikey');
if ($apiKey !== null) {
$queryParams['hapikey'] = $apiKey;
}
// this endpoint requires OAuth (access token)
if ($this->config->getAccessToken() !== null) {
$headers['Authorization'] = 'Bearer ' . $this->config->getAccessToken();
}
$defaultHeaders = [];
if ($this->config->getUserAgent()) {
$defaultHeaders['User-Agent'] = $this->config->getUserAgent();
}
$headers = array_merge(
$defaultHeaders,
$headerParams,
$headers
);
$query = \GuzzleHttp\Psr7\Query::build($queryParams);
return new Request(
'GET',
$this->config->getHost() . $resourcePath . ($query ? "?{$query}" : ''),
$headers,
$httpBody
);
}
/**
* Operation getPage
*
* List
*
* @param int $limit The maximum number of results to display per page. (optional, default to 10)
* @param string $after The paging cursor token of the last successfully read resource will be returned as the `paging.next.after` JSON property of a paged response containing more results. (optional)
* @param string[] $properties A comma separated list of the properties to be returned in the response. If any of the specified properties are not present on the requested object(s), they will be ignored. (optional)
* @param string[] $associations A comma separated list of object types to retrieve associated IDs for. If any of the specified associations do not exist, they will be ignored. (optional)
* @param bool $archived Whether to return only results that have been archived. (optional, default to false)
*
* @throws \HubSpot\Client\Crm\Deals\ApiException on non-2xx response
* @throws \InvalidArgumentException
* @return \HubSpot\Client\Crm\Deals\Model\CollectionResponseSimplePublicObjectWithAssociationsForwardPaging|\HubSpot\Client\Crm\Deals\Model\Error
*/
public function getPage($limit = 10, $after = null, $properties = null, $associations = null, $archived = false)
{
list($response) = $this->getPageWithHttpInfo($limit, $after, $properties, $associations, $archived);
return $response;
}
/**
* Operation getPageWithHttpInfo
*
* List
*
* @param int $limit The maximum number of results to display per page. (optional, default to 10)
* @param string $after The paging cursor token of the last successfully read resource will be returned as the `paging.next.after` JSON property of a paged response containing more results. (optional)
* @param string[] $properties A comma separated list of the properties to be returned in the response. If any of the specified properties are not present on the requested object(s), they will be ignored. (optional)
* @param string[] $associations A comma separated list of object types to retrieve associated IDs for. If any of the specified associations do not exist, they will be ignored. (optional)
* @param bool $archived Whether to return only results that have been archived. (optional, default to false)
*
* @throws \HubSpot\Client\Crm\Deals\ApiException on non-2xx response
* @throws \InvalidArgumentException
* @return array of \HubSpot\Client\Crm\Deals\Model\CollectionResponseSimplePublicObjectWithAssociationsForwardPaging|\HubSpot\Client\Crm\Deals\Model\Error, HTTP status code, HTTP response headers (array of strings)
*/
public function getPageWithHttpInfo($limit = 10, $after = null, $properties = null, $associations = null, $archived = false)
{
$request = $this->getPageRequest($limit, $after, $properties, $associations, $archived);
try {
$options = $this->createHttpClientOption();
try {
$response = $this->client->send($request, $options);
} catch (RequestException $e) {
throw new ApiException(
"[{$e->getCode()}] {$e->getMessage()}",
(int) $e->getCode(),
$e->getResponse() ? $e->getResponse()->getHeaders() : null,
$e->getResponse() ? (string) $e->getResponse()->getBody() : null
);
} catch (ConnectException $e) {
throw new ApiException(
"[{$e->getCode()}] {$e->getMessage()}",
(int) $e->getCode(),
null,
null
);
}
$statusCode = $response->getStatusCode();
if ($statusCode < 200 || $statusCode > 299) {
throw new ApiException(
sprintf(
'[%d] Error connecting to the API (%s)',
$statusCode,
(string) $request->getUri()
),
$statusCode,
$response->getHeaders(),
(string) $response->getBody()
);
}
switch($statusCode) {
case 200:
if ('\HubSpot\Client\Crm\Deals\Model\CollectionResponseSimplePublicObjectWithAssociationsForwardPaging' === '\SplFileObject') {
$content = $response->getBody(); //stream goes to serializer
} else {
$content = (string) $response->getBody();
}
return [
ObjectSerializer::deserialize($content, '\HubSpot\Client\Crm\Deals\Model\CollectionResponseSimplePublicObjectWithAssociationsForwardPaging', []),
$response->getStatusCode(),
$response->getHeaders()
];
default:
if ('\HubSpot\Client\Crm\Deals\Model\Error' === '\SplFileObject') {
$content = $response->getBody(); //stream goes to serializer
} else {
$content = (string) $response->getBody();
}
return [
ObjectSerializer::deserialize($content, '\HubSpot\Client\Crm\Deals\Model\Error', []),
$response->getStatusCode(),
$response->getHeaders()
];
}
$returnType = '\HubSpot\Client\Crm\Deals\Model\CollectionResponseSimplePublicObjectWithAssociationsForwardPaging';
if ($returnType === '\SplFileObject') {
$content = $response->getBody(); //stream goes to serializer
} else {
$content = (string) $response->getBody();
}
return [
ObjectSerializer::deserialize($content, $returnType, []),
$response->getStatusCode(),
$response->getHeaders()
];
} catch (ApiException $e) {
switch ($e->getCode()) {
case 200:
$data = ObjectSerializer::deserialize(
$e->getResponseBody(),
'\HubSpot\Client\Crm\Deals\Model\CollectionResponseSimplePublicObjectWithAssociationsForwardPaging',
$e->getResponseHeaders()
);
$e->setResponseObject($data);
break;
default:
$data = ObjectSerializer::deserialize(
$e->getResponseBody(),
'\HubSpot\Client\Crm\Deals\Model\Error',
$e->getResponseHeaders()
);
$e->setResponseObject($data);
break;
}
throw $e;
}
}
/**
* Operation getPageAsync
*
* List
*
* @param int $limit The maximum number of results to display per page. (optional, default to 10)
* @param string $after The paging cursor token of the last successfully read resource will be returned as the `paging.next.after` JSON property of a paged response containing more results. (optional)
* @param string[] $properties A comma separated list of the properties to be returned in the response. If any of the specified properties are not present on the requested object(s), they will be ignored. (optional)
* @param string[] $associations A comma separated list of object types to retrieve associated IDs for. If any of the specified associations do not exist, they will be ignored. (optional)
* @param bool $archived Whether to return only results that have been archived. (optional, default to false)
*
* @throws \InvalidArgumentException
* @return \GuzzleHttp\Promise\PromiseInterface
*/
public function getPageAsync($limit = 10, $after = null, $properties = null, $associations = null, $archived = false)
{
return $this->getPageAsyncWithHttpInfo($limit, $after, $properties, $associations, $archived)
->then(
function ($response) {
return $response[0];
}
);
}
/**
* Operation getPageAsyncWithHttpInfo
*
* List
*
* @param int $limit The maximum number of results to display per page. (optional, default to 10)
* @param string $after The paging cursor token of the last successfully read resource will be returned as the `paging.next.after` JSON property of a paged response containing more results. (optional)
* @param string[] $properties A comma separated list of the properties to be returned in the response. If any of the specified properties are not present on the requested object(s), they will be ignored. (optional)
* @param string[] $associations A comma separated list of object types to retrieve associated IDs for. If any of the specified associations do not exist, they will be ignored. (optional)
* @param bool $archived Whether to return only results that have been archived. (optional, default to false)
*
* @throws \InvalidArgumentException
* @return \GuzzleHttp\Promise\PromiseInterface
*/
public function getPageAsyncWithHttpInfo($limit = 10, $after = null, $properties = null, $associations = null, $archived = false)
{
$returnType = '\HubSpot\Client\Crm\Deals\Model\CollectionResponseSimplePublicObjectWithAssociationsForwardPaging';
$request = $this->getPageRequest($limit, $after, $properties, $associations, $archived);
return $this->client
->sendAsync($request, $this->createHttpClientOption())
->then(
function ($response) use ($returnType) {
if ($returnType === '\SplFileObject') {
$content = $response->getBody(); //stream goes to serializer
} else {
$content = (string) $response->getBody();
}
return [
ObjectSerializer::deserialize($content, $returnType, []),
$response->getStatusCode(),
$response->getHeaders()
];
},
function ($exception) {
$response = $exception->getResponse();
$statusCode = $response->getStatusCode();
throw new ApiException(
sprintf(
'[%d] Error connecting to the API (%s)',
$statusCode,
$exception->getRequest()->getUri()
),
$statusCode,
$response->getHeaders(),
(string) $response->getBody()
);
}
);
}
/**
* Create request for operation 'getPage'
*
* @param int $limit The maximum number of results to display per page. (optional, default to 10)
* @param string $after The paging cursor token of the last successfully read resource will be returned as the `paging.next.after` JSON property of a paged response containing more results. (optional)
* @param string[] $properties A comma separated list of the properties to be returned in the response. If any of the specified properties are not present on the requested object(s), they will be ignored. (optional)
* @param string[] $associations A comma separated list of object types to retrieve associated IDs for. If any of the specified associations do not exist, they will be ignored. (optional)
* @param bool $archived Whether to return only results that have been archived. (optional, default to false)
*
* @throws \InvalidArgumentException
* @return \GuzzleHttp\Psr7\Request
*/
public function getPageRequest($limit = 10, $after = null, $properties = null, $associations = null, $archived = false)
{
$res...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.034242023,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: master","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"Editor for custom.log","depth":4,"bounds":{"left":0.5475399,"top":0.0726257,"width":0.44082448,"height":0.9066241},"on_screen":true,"role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Reader Mode","depth":4,"bounds":{"left":0.45478722,"top":0.17318435,"width":0.034574468,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Analyzing…","depth":4,"bounds":{"left":0.5053192,"top":0.17478053,"width":0.019946808,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXTextArea","text":"<?php\n/**\n * BasicApi\n * PHP version 7.3\n *\n * @category Class\n * @package HubSpot\\Client\\Crm\\Deals\n * @author OpenAPI Generator team\n * @link https://openapi-generator.tech\n */\n\n/**\n * Deals\n *\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: v3\n * Generated by: https://openapi-generator.tech\n * OpenAPI Generator version: 5.3.0\n */\n\n/**\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\nnamespace HubSpot\\Client\\Crm\\Deals\\Api;\n\nuse GuzzleHttp\\Client;\nuse GuzzleHttp\\ClientInterface;\nuse GuzzleHttp\\Exception\\RequestException;\nuse GuzzleHttp\\Exception\\ConnectException;\nuse GuzzleHttp\\Psr7\\MultipartStream;\nuse GuzzleHttp\\Psr7\\Request;\nuse GuzzleHttp\\RequestOptions;\nuse HubSpot\\Client\\Crm\\Deals\\ApiException;\nuse HubSpot\\Client\\Crm\\Deals\\Configuration;\nuse HubSpot\\Client\\Crm\\Deals\\HeaderSelector;\nuse HubSpot\\Client\\Crm\\Deals\\ObjectSerializer;\n\n/**\n * BasicApi Class Doc Comment\n *\n * @category Class\n * @package HubSpot\\Client\\Crm\\Deals\n * @author OpenAPI Generator team\n * @link https://openapi-generator.tech\n */\nclass BasicApi\n{\n /**\n * @var ClientInterface\n */\n protected $client;\n\n /**\n * @var Configuration\n */\n protected $config;\n\n /**\n * @var HeaderSelector\n */\n protected $headerSelector;\n\n /**\n * @var int Host index\n */\n protected $hostIndex;\n\n /**\n * @param ClientInterface $client\n * @param Configuration $config\n * @param HeaderSelector $selector\n * @param int $hostIndex (Optional) host index to select the list of hosts if defined in the OpenAPI spec\n */\n public function __construct(\n ClientInterface $client = null,\n Configuration $config = null,\n HeaderSelector $selector = null,\n $hostIndex = 0\n ) {\n $this->client = $client ?: new Client();\n $this->config = $config ?: new Configuration();\n $this->headerSelector = $selector ?: new HeaderSelector();\n $this->hostIndex = $hostIndex;\n }\n\n /**\n * Set the host index\n *\n * @param int $hostIndex Host index (required)\n */\n public function setHostIndex($hostIndex): void\n {\n $this->hostIndex = $hostIndex;\n }\n\n /**\n * Get the host index\n *\n * @return int Host index\n */\n public function getHostIndex()\n {\n return $this->hostIndex;\n }\n\n /**\n * @return Configuration\n */\n public function getConfig()\n {\n return $this->config;\n }\n\n /**\n * Operation archive\n *\n * Archive\n *\n * @param string $deal_id deal_id (required)\n *\n * @throws \\HubSpot\\Client\\Crm\\Deals\\ApiException on non-2xx response\n * @throws \\InvalidArgumentException\n * @return void\n */\n public function archive($deal_id)\n {\n $this->archiveWithHttpInfo($deal_id);\n }\n\n /**\n * Operation archiveWithHttpInfo\n *\n * Archive\n *\n * @param string $deal_id (required)\n *\n * @throws \\HubSpot\\Client\\Crm\\Deals\\ApiException on non-2xx response\n * @throws \\InvalidArgumentException\n * @return array of null, HTTP status code, HTTP response headers (array of strings)\n */\n public function archiveWithHttpInfo($deal_id)\n {\n $request = $this->archiveRequest($deal_id);\n\n try {\n $options = $this->createHttpClientOption();\n try {\n $response = $this->client->send($request, $options);\n } catch (RequestException $e) {\n throw new ApiException(\n \"[{$e->getCode()}] {$e->getMessage()}\",\n (int) $e->getCode(),\n $e->getResponse() ? $e->getResponse()->getHeaders() : null,\n $e->getResponse() ? (string) $e->getResponse()->getBody() : null\n );\n } catch (ConnectException $e) {\n throw new ApiException(\n \"[{$e->getCode()}] {$e->getMessage()}\",\n (int) $e->getCode(),\n null,\n null\n );\n }\n\n $statusCode = $response->getStatusCode();\n\n if ($statusCode < 200 || $statusCode > 299) {\n throw new ApiException(\n sprintf(\n '[%d] Error connecting to the API (%s)',\n $statusCode,\n (string) $request->getUri()\n ),\n $statusCode,\n $response->getHeaders(),\n (string) $response->getBody()\n );\n }\n\n return [null, $statusCode, $response->getHeaders()];\n\n } catch (ApiException $e) {\n switch ($e->getCode()) {\n default:\n $data = ObjectSerializer::deserialize(\n $e->getResponseBody(),\n '\\HubSpot\\Client\\Crm\\Deals\\Model\\Error',\n $e->getResponseHeaders()\n );\n $e->setResponseObject($data);\n break;\n }\n throw $e;\n }\n }\n\n /**\n * Operation archiveAsync\n *\n * Archive\n *\n * @param string $deal_id (required)\n *\n * @throws \\InvalidArgumentException\n * @return \\GuzzleHttp\\Promise\\PromiseInterface\n */\n public function archiveAsync($deal_id)\n {\n return $this->archiveAsyncWithHttpInfo($deal_id)\n ->then(\n function ($response) {\n return $response[0];\n }\n );\n }\n\n /**\n * Operation archiveAsyncWithHttpInfo\n *\n * Archive\n *\n * @param string $deal_id (required)\n *\n * @throws \\InvalidArgumentException\n * @return \\GuzzleHttp\\Promise\\PromiseInterface\n */\n public function archiveAsyncWithHttpInfo($deal_id)\n {\n $returnType = '';\n $request = $this->archiveRequest($deal_id);\n\n return $this->client\n ->sendAsync($request, $this->createHttpClientOption())\n ->then(\n function ($response) use ($returnType) {\n return [null, $response->getStatusCode(), $response->getHeaders()];\n },\n function ($exception) {\n $response = $exception->getResponse();\n $statusCode = $response->getStatusCode();\n throw new ApiException(\n sprintf(\n '[%d] Error connecting to the API (%s)',\n $statusCode,\n $exception->getRequest()->getUri()\n ),\n $statusCode,\n $response->getHeaders(),\n (string) $response->getBody()\n );\n }\n );\n }\n\n /**\n * Create request for operation 'archive'\n *\n * @param string $deal_id (required)\n *\n * @throws \\InvalidArgumentException\n * @return \\GuzzleHttp\\Psr7\\Request\n */\n public function archiveRequest($deal_id)\n {\n // verify the required parameter 'deal_id' is set\n if ($deal_id === null || (is_array($deal_id) && count($deal_id) === 0)) {\n throw new \\InvalidArgumentException(\n 'Missing the required parameter $deal_id when calling archive'\n );\n }\n\n $resourcePath = '/crm/v3/objects/deals/{dealId}';\n $formParams = [];\n $queryParams = [];\n $headerParams = [];\n $httpBody = '';\n $multipart = false;\n\n\n\n // path params\n if ($deal_id !== null) {\n $resourcePath = str_replace(\n '{' . 'dealId' . '}',\n ObjectSerializer::toPathValue($deal_id),\n $resourcePath\n );\n }\n\n\n if ($multipart) {\n $headers = $this->headerSelector->selectHeadersForMultipart(\n ['*/*']\n );\n } else {\n $headers = $this->headerSelector->selectHeaders(\n ['*/*'],\n []\n );\n }\n\n // for model (json/xml)\n if (count($formParams) > 0) {\n if ($multipart) {\n $multipartContents = [];\n foreach ($formParams as $formParamName => $formParamValue) {\n $formParamValueItems = is_array($formParamValue) ? $formParamValue : [$formParamValue];\n foreach ($formParamValueItems as $formParamValueItem) {\n $multipartContents[] = [\n 'name' => $formParamName,\n 'contents' => $formParamValueItem\n ];\n }\n }\n // for HTTP post (form)\n $httpBody = new MultipartStream($multipartContents);\n\n } elseif ($headers['Content-Type'] === 'application/json') {\n $httpBody = \\GuzzleHttp\\json_encode($formParams);\n\n } else {\n // for HTTP post (form)\n $httpBody = \\GuzzleHttp\\Psr7\\Query::build($formParams);\n }\n }\n\n // this endpoint requires API key authentication\n $apiKey = $this->config->getApiKeyWithPrefix('hapikey');\n if ($apiKey !== null) {\n $queryParams['hapikey'] = $apiKey;\n }\n // this endpoint requires OAuth (access token)\n if ($this->config->getAccessToken() !== null) {\n $headers['Authorization'] = 'Bearer ' . $this->config->getAccessToken();\n }\n\n $defaultHeaders = [];\n if ($this->config->getUserAgent()) {\n $defaultHeaders['User-Agent'] = $this->config->getUserAgent();\n }\n\n $headers = array_merge(\n $defaultHeaders,\n $headerParams,\n $headers\n );\n\n $query = \\GuzzleHttp\\Psr7\\Query::build($queryParams);\n return new Request(\n 'DELETE',\n $this->config->getHost() . $resourcePath . ($query ? \"?{$query}\" : ''),\n $headers,\n $httpBody\n );\n }\n\n /**\n * Operation create\n *\n * Create\n *\n * @param \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectInput $simple_public_object_input simple_public_object_input (required)\n *\n * @throws \\HubSpot\\Client\\Crm\\Deals\\ApiException on non-2xx response\n * @throws \\InvalidArgumentException\n * @return \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObject|\\HubSpot\\Client\\Crm\\Deals\\Model\\Error\n */\n public function create($simple_public_object_input)\n {\n list($response) = $this->createWithHttpInfo($simple_public_object_input);\n return $response;\n }\n\n /**\n * Operation createWithHttpInfo\n *\n * Create\n *\n * @param \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectInput $simple_public_object_input (required)\n *\n * @throws \\HubSpot\\Client\\Crm\\Deals\\ApiException on non-2xx response\n * @throws \\InvalidArgumentException\n * @return array of \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObject|\\HubSpot\\Client\\Crm\\Deals\\Model\\Error, HTTP status code, HTTP response headers (array of strings)\n */\n public function createWithHttpInfo($simple_public_object_input)\n {\n $request = $this->createRequest($simple_public_object_input);\n\n try {\n $options = $this->createHttpClientOption();\n try {\n $response = $this->client->send($request, $options);\n } catch (RequestException $e) {\n throw new ApiException(\n \"[{$e->getCode()}] {$e->getMessage()}\",\n (int) $e->getCode(),\n $e->getResponse() ? $e->getResponse()->getHeaders() : null,\n $e->getResponse() ? (string) $e->getResponse()->getBody() : null\n );\n } catch (ConnectException $e) {\n throw new ApiException(\n \"[{$e->getCode()}] {$e->getMessage()}\",\n (int) $e->getCode(),\n null,\n null\n );\n }\n\n $statusCode = $response->getStatusCode();\n\n if ($statusCode < 200 || $statusCode > 299) {\n throw new ApiException(\n sprintf(\n '[%d] Error connecting to the API (%s)',\n $statusCode,\n (string) $request->getUri()\n ),\n $statusCode,\n $response->getHeaders(),\n (string) $response->getBody()\n );\n }\n\n switch($statusCode) {\n case 201:\n if ('\\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObject' === '\\SplFileObject') {\n $content = $response->getBody(); //stream goes to serializer\n } else {\n $content = (string) $response->getBody();\n }\n\n return [\n ObjectSerializer::deserialize($content, '\\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObject', []),\n $response->getStatusCode(),\n $response->getHeaders()\n ];\n default:\n if ('\\HubSpot\\Client\\Crm\\Deals\\Model\\Error' === '\\SplFileObject') {\n $content = $response->getBody(); //stream goes to serializer\n } else {\n $content = (string) $response->getBody();\n }\n\n return [\n ObjectSerializer::deserialize($content, '\\HubSpot\\Client\\Crm\\Deals\\Model\\Error', []),\n $response->getStatusCode(),\n $response->getHeaders()\n ];\n }\n\n $returnType = '\\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObject';\n if ($returnType === '\\SplFileObject') {\n $content = $response->getBody(); //stream goes to serializer\n } else {\n $content = (string) $response->getBody();\n }\n\n return [\n ObjectSerializer::deserialize($content, $returnType, []),\n $response->getStatusCode(),\n $response->getHeaders()\n ];\n\n } catch (ApiException $e) {\n switch ($e->getCode()) {\n case 201:\n $data = ObjectSerializer::deserialize(\n $e->getResponseBody(),\n '\\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObject',\n $e->getResponseHeaders()\n );\n $e->setResponseObject($data);\n break;\n default:\n $data = ObjectSerializer::deserialize(\n $e->getResponseBody(),\n '\\HubSpot\\Client\\Crm\\Deals\\Model\\Error',\n $e->getResponseHeaders()\n );\n $e->setResponseObject($data);\n break;\n }\n throw $e;\n }\n }\n\n /**\n * Operation createAsync\n *\n * Create\n *\n * @param \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectInput $simple_public_object_input (required)\n *\n * @throws \\InvalidArgumentException\n * @return \\GuzzleHttp\\Promise\\PromiseInterface\n */\n public function createAsync($simple_public_object_input)\n {\n return $this->createAsyncWithHttpInfo($simple_public_object_input)\n ->then(\n function ($response) {\n return $response[0];\n }\n );\n }\n\n /**\n * Operation createAsyncWithHttpInfo\n *\n * Create\n *\n * @param \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectInput $simple_public_object_input (required)\n *\n * @throws \\InvalidArgumentException\n * @return \\GuzzleHttp\\Promise\\PromiseInterface\n */\n public function createAsyncWithHttpInfo($simple_public_object_input)\n {\n $returnType = '\\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObject';\n $request = $this->createRequest($simple_public_object_input);\n\n return $this->client\n ->sendAsync($request, $this->createHttpClientOption())\n ->then(\n function ($response) use ($returnType) {\n if ($returnType === '\\SplFileObject') {\n $content = $response->getBody(); //stream goes to serializer\n } else {\n $content = (string) $response->getBody();\n }\n\n return [\n ObjectSerializer::deserialize($content, $returnType, []),\n $response->getStatusCode(),\n $response->getHeaders()\n ];\n },\n function ($exception) {\n $response = $exception->getResponse();\n $statusCode = $response->getStatusCode();\n throw new ApiException(\n sprintf(\n '[%d] Error connecting to the API (%s)',\n $statusCode,\n $exception->getRequest()->getUri()\n ),\n $statusCode,\n $response->getHeaders(),\n (string) $response->getBody()\n );\n }\n );\n }\n\n /**\n * Create request for operation 'create'\n *\n * @param \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectInput $simple_public_object_input (required)\n *\n * @throws \\InvalidArgumentException\n * @return \\GuzzleHttp\\Psr7\\Request\n */\n public function createRequest($simple_public_object_input)\n {\n // verify the required parameter 'simple_public_object_input' is set\n if ($simple_public_object_input === null || (is_array($simple_public_object_input) && count($simple_public_object_input) === 0)) {\n throw new \\InvalidArgumentException(\n 'Missing the required parameter $simple_public_object_input when calling create'\n );\n }\n\n $resourcePath = '/crm/v3/objects/deals';\n $formParams = [];\n $queryParams = [];\n $headerParams = [];\n $httpBody = '';\n $multipart = false;\n\n\n\n\n\n if ($multipart) {\n $headers = $this->headerSelector->selectHeadersForMultipart(\n ['application/json', '*/*']\n );\n } else {\n $headers = $this->headerSelector->selectHeaders(\n ['application/json', '*/*'],\n ['application/json']\n );\n }\n\n // for model (json/xml)\n if (isset($simple_public_object_input)) {\n if ($headers['Content-Type'] === 'application/json') {\n $httpBody = \\GuzzleHttp\\json_encode(ObjectSerializer::sanitizeForSerialization($simple_public_object_input));\n } else {\n $httpBody = $simple_public_object_input;\n }\n } elseif (count($formParams) > 0) {\n if ($multipart) {\n $multipartContents = [];\n foreach ($formParams as $formParamName => $formParamValue) {\n $formParamValueItems = is_array($formParamValue) ? $formParamValue : [$formParamValue];\n foreach ($formParamValueItems as $formParamValueItem) {\n $multipartContents[] = [\n 'name' => $formParamName,\n 'contents' => $formParamValueItem\n ];\n }\n }\n // for HTTP post (form)\n $httpBody = new MultipartStream($multipartContents);\n\n } elseif ($headers['Content-Type'] === 'application/json') {\n $httpBody = \\GuzzleHttp\\json_encode($formParams);\n\n } else {\n // for HTTP post (form)\n $httpBody = \\GuzzleHttp\\Psr7\\Query::build($formParams);\n }\n }\n\n // this endpoint requires API key authentication\n $apiKey = $this->config->getApiKeyWithPrefix('hapikey');\n if ($apiKey !== null) {\n $queryParams['hapikey'] = $apiKey;\n }\n // this endpoint requires OAuth (access token)\n if ($this->config->getAccessToken() !== null) {\n $headers['Authorization'] = 'Bearer ' . $this->config->getAccessToken();\n }\n\n $defaultHeaders = [];\n if ($this->config->getUserAgent()) {\n $defaultHeaders['User-Agent'] = $this->config->getUserAgent();\n }\n\n $headers = array_merge(\n $defaultHeaders,\n $headerParams,\n $headers\n );\n\n $query = \\GuzzleHttp\\Psr7\\Query::build($queryParams);\n return new Request(\n 'POST',\n $this->config->getHost() . $resourcePath . ($query ? \"?{$query}\" : ''),\n $headers,\n $httpBody\n );\n }\n\n /**\n * Operation getById\n *\n * Read\n *\n * @param string $deal_id deal_id (required)\n * @param string[] $properties A comma separated list of the properties to be returned in the response. If any of the specified properties are not present on the requested object(s), they will be ignored. (optional)\n * @param string[] $associations A comma separated list of object types to retrieve associated IDs for. If any of the specified associations do not exist, they will be ignored. (optional)\n * @param bool $archived Whether to return only results that have been archived. (optional, default to false)\n * @param string $id_property The name of a property whose values are unique for this object type (optional)\n *\n * @throws \\HubSpot\\Client\\Crm\\Deals\\ApiException on non-2xx response\n * @throws \\InvalidArgumentException\n * @return \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations|\\HubSpot\\Client\\Crm\\Deals\\Model\\Error\n */\n public function getById($deal_id, $properties = null, $associations = null, $archived = false, $id_property = null)\n {\n list($response) = $this->getByIdWithHttpInfo($deal_id, $properties, $associations, $archived, $id_property);\n return $response;\n }\n\n /**\n * Operation getByIdWithHttpInfo\n *\n * Read\n *\n * @param string $deal_id (required)\n * @param string[] $properties A comma separated list of the properties to be returned in the response. If any of the specified properties are not present on the requested object(s), they will be ignored. (optional)\n * @param string[] $associations A comma separated list of object types to retrieve associated IDs for. If any of the specified associations do not exist, they will be ignored. (optional)\n * @param bool $archived Whether to return only results that have been archived. (optional, default to false)\n * @param string $id_property The name of a property whose values are unique for this object type (optional)\n *\n * @throws \\HubSpot\\Client\\Crm\\Deals\\ApiException on non-2xx response\n * @throws \\InvalidArgumentException\n * @return array of \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations|\\HubSpot\\Client\\Crm\\Deals\\Model\\Error, HTTP status code, HTTP response headers (array of strings)\n */\n public function getByIdWithHttpInfo($deal_id, $properties = null, $associations = null, $archived = false, $id_property = null)\n {\n $request = $this->getByIdRequest($deal_id, $properties, $associations, $archived, $id_property);\n\n try {\n $options = $this->createHttpClientOption();\n try {\n $response = $this->client->send($request, $options);\n } catch (RequestException $e) {\n throw new ApiException(\n \"[{$e->getCode()}] {$e->getMessage()}\",\n (int) $e->getCode(),\n $e->getResponse() ? $e->getResponse()->getHeaders() : null,\n $e->getResponse() ? (string) $e->getResponse()->getBody() : null\n );\n } catch (ConnectException $e) {\n throw new ApiException(\n \"[{$e->getCode()}] {$e->getMessage()}\",\n (int) $e->getCode(),\n null,\n null\n );\n }\n\n $statusCode = $response->getStatusCode();\n\n if ($statusCode < 200 || $statusCode > 299) {\n throw new ApiException(\n sprintf(\n '[%d] Error connecting to the API (%s)',\n $statusCode,\n (string) $request->getUri()\n ),\n $statusCode,\n $response->getHeaders(),\n (string) $response->getBody()\n );\n }\n\n switch($statusCode) {\n case 200:\n if ('\\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations' === '\\SplFileObject') {\n $content = $response->getBody(); //stream goes to serializer\n } else {\n $content = (string) $response->getBody();\n }\n\n return [\n ObjectSerializer::deserialize($content, '\\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations', []),\n $response->getStatusCode(),\n $response->getHeaders()\n ];\n default:\n if ('\\HubSpot\\Client\\Crm\\Deals\\Model\\Error' === '\\SplFileObject') {\n $content = $response->getBody(); //stream goes to serializer\n } else {\n $content = (string) $response->getBody();\n }\n\n return [\n ObjectSerializer::deserialize($content, '\\HubSpot\\Client\\Crm\\Deals\\Model\\Error', []),\n $response->getStatusCode(),\n $response->getHeaders()\n ];\n }\n\n $returnType = '\\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations';\n if ($returnType === '\\SplFileObject') {\n $content = $response->getBody(); //stream goes to serializer\n } else {\n $content = (string) $response->getBody();\n }\n\n return [\n ObjectSerializer::deserialize($content, $returnType, []),\n $response->getStatusCode(),\n $response->getHeaders()\n ];\n\n } catch (ApiException $e) {\n switch ($e->getCode()) {\n case 200:\n $data = ObjectSerializer::deserialize(\n $e->getResponseBody(),\n '\\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations',\n $e->getResponseHeaders()\n );\n $e->setResponseObject($data);\n break;\n default:\n $data = ObjectSerializer::deserialize(\n $e->getResponseBody(),\n '\\HubSpot\\Client\\Crm\\Deals\\Model\\Error',\n $e->getResponseHeaders()\n );\n $e->setResponseObject($data);\n break;\n }\n throw $e;\n }\n }\n\n /**\n * Operation getByIdAsync\n *\n * Read\n *\n * @param string $deal_id (required)\n * @param string[] $properties A comma separated list of the properties to be returned in the response. If any of the specified properties are not present on the requested object(s), they will be ignored. (optional)\n * @param string[] $associations A comma separated list of object types to retrieve associated IDs for. If any of the specified associations do not exist, they will be ignored. (optional)\n * @param bool $archived Whether to return only results that have been archived. (optional, default to false)\n * @param string $id_property The name of a property whose values are unique for this object type (optional)\n *\n * @throws \\InvalidArgumentException\n * @return \\GuzzleHttp\\Promise\\PromiseInterface\n */\n public function getByIdAsync($deal_id, $properties = null, $associations = null, $archived = false, $id_property = null)\n {\n return $this->getByIdAsyncWithHttpInfo($deal_id, $properties, $associations, $archived, $id_property)\n ->then(\n function ($response) {\n return $response[0];\n }\n );\n }\n\n /**\n * Operation getByIdAsyncWithHttpInfo\n *\n * Read\n *\n * @param string $deal_id (required)\n * @param string[] $properties A comma separated list of the properties to be returned in the response. If any of the specified properties are not present on the requested object(s), they will be ignored. (optional)\n * @param string[] $associations A comma separated list of object types to retrieve associated IDs for. If any of the specified associations do not exist, they will be ignored. (optional)\n * @param bool $archived Whether to return only results that have been archived. (optional, default to false)\n * @param string $id_property The name of a property whose values are unique for this object type (optional)\n *\n * @throws \\InvalidArgumentException\n * @return \\GuzzleHttp\\Promise\\PromiseInterface\n */\n public function getByIdAsyncWithHttpInfo($deal_id, $properties = null, $associations = null, $archived = false, $id_property = null)\n {\n $returnType = '\\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations';\n $request = $this->getByIdRequest($deal_id, $properties, $associations, $archived, $id_property);\n\n return $this->client\n ->sendAsync($request, $this->createHttpClientOption())\n ->then(\n function ($response) use ($returnType) {\n if ($returnType === '\\SplFileObject') {\n $content = $response->getBody(); //stream goes to serializer\n } else {\n $content = (string) $response->getBody();\n }\n\n return [\n ObjectSerializer::deserialize($content, $returnType, []),\n $response->getStatusCode(),\n $response->getHeaders()\n ];\n },\n function ($exception) {\n $response = $exception->getResponse();\n $statusCode = $response->getStatusCode();\n throw new ApiException(\n sprintf(\n '[%d] Error connecting to the API (%s)',\n $statusCode,\n $exception->getRequest()->getUri()\n ),\n $statusCode,\n $response->getHeaders(),\n (string) $response->getBody()\n );\n }\n );\n }\n\n /**\n * Create request for operation 'getById'\n *\n * @param string $deal_id (required)\n * @param string[] $properties A comma separated list of the properties to be returned in the response. If any of the specified properties are not present on the requested object(s), they will be ignored. (optional)\n * @param string[] $associations A comma separated list of object types to retrieve associated IDs for. If any of the specified associations do not exist, they will be ignored. (optional)\n * @param bool $archived Whether to return only results that have been archived. (optional, default to false)\n * @param string $id_property The name of a property whose values are unique for this object type (optional)\n *\n * @throws \\InvalidArgumentException\n * @return \\GuzzleHttp\\Psr7\\Request\n */\n public function getByIdRequest($deal_id, $properties = null, $associations = null, $archived = false, $id_property = null)\n {\n // verify the required parameter 'deal_id' is set\n if ($deal_id === null || (is_array($deal_id) && count($deal_id) === 0)) {\n throw new \\InvalidArgumentException(\n 'Missing the required parameter $deal_id when calling getById'\n );\n }\n\n $resourcePath = '/crm/v3/objects/deals/{dealId}';\n $formParams = [];\n $queryParams = [];\n $headerParams = [];\n $httpBody = '';\n $multipart = false;\n\n // query params\n if ($properties !== null) {\n if('form' === 'form' && is_array($properties)) {\n foreach($properties as $key => $value) {\n $queryParams[$key] = $value;\n }\n }\n else {\n $queryParams['properties'] = $properties;\n }\n }\n // query params\n if ($associations !== null) {\n if('form' === 'form' && is_array($associations)) {\n foreach($associations as $key => $value) {\n $queryParams[$key] = $value;\n }\n }\n else {\n $queryParams['associations'] = $associations;\n }\n }\n // query params\n if ($archived !== null) {\n if('form' === 'form' && is_array($archived)) {\n foreach($archived as $key => $value) {\n $queryParams[$key] = $value;\n }\n }\n else {\n $queryParams['archived'] = $archived;\n }\n }\n // query params\n if ($id_property !== null) {\n if('form' === 'form' && is_array($id_property)) {\n foreach($id_property as $key => $value) {\n $queryParams[$key] = $value;\n }\n }\n else {\n $queryParams['idProperty'] = $id_property;\n }\n }\n\n\n // path params\n if ($deal_id !== null) {\n $resourcePath = str_replace(\n '{' . 'dealId' . '}',\n ObjectSerializer::toPathValue($deal_id),\n $resourcePath\n );\n }\n\n\n if ($multipart) {\n $headers = $this->headerSelector->selectHeadersForMultipart(\n ['application/json', '*/*']\n );\n } else {\n $headers = $this->headerSelector->selectHeaders(\n ['application/json', '*/*'],\n []\n );\n }\n\n // for model (json/xml)\n if (count($formParams) > 0) {\n if ($multipart) {\n $multipartContents = [];\n foreach ($formParams as $formParamName => $formParamValue) {\n $formParamValueItems = is_array($formParamValue) ? $formParamValue : [$formParamValue];\n foreach ($formParamValueItems as $formParamValueItem) {\n $multipartContents[] = [\n 'name' => $formParamName,\n 'contents' => $formParamValueItem\n ];\n }\n }\n // for HTTP post (form)\n $httpBody = new MultipartStream($multipartContents);\n\n } elseif ($headers['Content-Type'] === 'application/json') {\n $httpBody = \\GuzzleHttp\\json_encode($formParams);\n\n } else {\n // for HTTP post (form)\n $httpBody = \\GuzzleHttp\\Psr7\\Query::build($formParams);\n }\n }\n\n // this endpoint requires API key authentication\n $apiKey = $this->config->getApiKeyWithPrefix('hapikey');\n if ($apiKey !== null) {\n $queryParams['hapikey'] = $apiKey;\n }\n // this endpoint requires OAuth (access token)\n if ($this->config->getAccessToken() !== null) {\n $headers['Authorization'] = 'Bearer ' . $this->config->getAccessToken();\n }\n\n $defaultHeaders = [];\n if ($this->config->getUserAgent()) {\n $defaultHeaders['User-Agent'] = $this->config->getUserAgent();\n }\n\n $headers = array_merge(\n $defaultHeaders,\n $headerParams,\n $headers\n );\n\n $query = \\GuzzleHttp\\Psr7\\Query::build($queryParams);\n return new Request(\n 'GET',\n $this->config->getHost() . $resourcePath . ($query ? \"?{$query}\" : ''),\n $headers,\n $httpBody\n );\n }\n\n /**\n * Operation getPage\n *\n * List\n *\n * @param int $limit The maximum number of results to display per page. (optional, default to 10)\n * @param string $after The paging cursor token of the last successfully read resource will be returned as the `paging.next.after` JSON property of a paged response containing more results. (optional)\n * @param string[] $properties A comma separated list of the properties to be returned in the response. If any of the specified properties are not present on the requested object(s), they will be ignored. (optional)\n * @param string[] $associations A comma separated list of object types to retrieve associated IDs for. If any of the specified associations do not exist, they will be ignored. (optional)\n * @param bool $archived Whether to return only results that have been archived. (optional, default to false)\n *\n * @throws \\HubSpot\\Client\\Crm\\Deals\\ApiException on non-2xx response\n * @throws \\InvalidArgumentException\n * @return \\HubSpot\\Client\\Crm\\Deals\\Model\\CollectionResponseSimplePublicObjectWithAssociationsForwardPaging|\\HubSpot\\Client\\Crm\\Deals\\Model\\Error\n */\n public function getPage($limit = 10, $after = null, $properties = null, $associations = null, $archived = false)\n {\n list($response) = $this->getPageWithHttpInfo($limit, $after, $properties, $associations, $archived);\n return $response;\n }\n\n /**\n * Operation getPageWithHttpInfo\n *\n * List\n *\n * @param int $limit The maximum number of results to display per page. (optional, default to 10)\n * @param string $after The paging cursor token of the last successfully read resource will be returned as the `paging.next.after` JSON property of a paged response containing more results. (optional)\n * @param string[] $properties A comma separated list of the properties to be returned in the response. If any of the specified properties are not present on the requested object(s), they will be ignored. (optional)\n * @param string[] $associations A comma separated list of object types to retrieve associated IDs for. If any of the specified associations do not exist, they will be ignored. (optional)\n * @param bool $archived Whether to return only results that have been archived. (optional, default to false)\n *\n * @throws \\HubSpot\\Client\\Crm\\Deals\\ApiException on non-2xx response\n * @throws \\InvalidArgumentException\n * @return array of \\HubSpot\\Client\\Crm\\Deals\\Model\\CollectionResponseSimplePublicObjectWithAssociationsForwardPaging|\\HubSpot\\Client\\Crm\\Deals\\Model\\Error, HTTP status code, HTTP response headers (array of strings)\n */\n public function getPageWithHttpInfo($limit = 10, $after = null, $properties = null, $associations = null, $archived = false)\n {\n $request = $this->getPageRequest($limit, $after, $properties, $associations, $archived);\n\n try {\n $options = $this->createHttpClientOption();\n try {\n $response = $this->client->send($request, $options);\n } catch (RequestException $e) {\n throw new ApiException(\n \"[{$e->getCode()}] {$e->getMessage()}\",\n (int) $e->getCode(),\n $e->getResponse() ? $e->getResponse()->getHeaders() : null,\n $e->getResponse() ? (string) $e->getResponse()->getBody() : null\n );\n } catch (ConnectException $e) {\n throw new ApiException(\n \"[{$e->getCode()}] {$e->getMessage()}\",\n (int) $e->getCode(),\n null,\n null\n );\n }\n\n $statusCode = $response->getStatusCode();\n\n if ($statusCode < 200 || $statusCode > 299) {\n throw new ApiException(\n sprintf(\n '[%d] Error connecting to the API (%s)',\n $statusCode,\n (string) $request->getUri()\n ),\n $statusCode,\n $response->getHeaders(),\n (string) $response->getBody()\n );\n }\n\n switch($statusCode) {\n case 200:\n if ('\\HubSpot\\Client\\Crm\\Deals\\Model\\CollectionResponseSimplePublicObjectWithAssociationsForwardPaging' === '\\SplFileObject') {\n $content = $response->getBody(); //stream goes to serializer\n } else {\n $content = (string) $response->getBody();\n }\n\n return [\n ObjectSerializer::deserialize($content, '\\HubSpot\\Client\\Crm\\Deals\\Model\\CollectionResponseSimplePublicObjectWithAssociationsForwardPaging', []),\n $response->getStatusCode(),\n $response->getHeaders()\n ];\n default:\n if ('\\HubSpot\\Client\\Crm\\Deals\\Model\\Error' === '\\SplFileObject') {\n $content = $response->getBody(); //stream goes to serializer\n } else {\n $content = (string) $response->getBody();\n }\n\n return [\n ObjectSerializer::deserialize($content, '\\HubSpot\\Client\\Crm\\Deals\\Model\\Error', []),\n $response->getStatusCode(),\n $response->getHeaders()\n ];\n }\n\n $returnType = '\\HubSpot\\Client\\Crm\\Deals\\Model\\CollectionResponseSimplePublicObjectWithAssociationsForwardPaging';\n if ($returnType === '\\SplFileObject') {\n $content = $response->getBody(); //stream goes to serializer\n } else {\n $content = (string) $response->getBody();\n }\n\n return [\n ObjectSerializer::deserialize($content, $returnType, []),\n $response->getStatusCode(),\n $response->getHeaders()\n ];\n\n } catch (ApiException $e) {\n switch ($e->getCode()) {\n case 200:\n $data = ObjectSerializer::deserialize(\n $e->getResponseBody(),\n '\\HubSpot\\Client\\Crm\\Deals\\Model\\CollectionResponseSimplePublicObjectWithAssociationsForwardPaging',\n $e->getResponseHeaders()\n );\n $e->setResponseObject($data);\n break;\n default:\n $data = ObjectSerializer::deserialize(\n $e->getResponseBody(),\n '\\HubSpot\\Client\\Crm\\Deals\\Model\\Error',\n $e->getResponseHeaders()\n );\n $e->setResponseObject($data);\n break;\n }\n throw $e;\n }\n }\n\n /**\n * Operation getPageAsync\n *\n * List\n *\n * @param int $limit The maximum number of results to display per page. (optional, default to 10)\n * @param string $after The paging cursor token of the last successfully read resource will be returned as the `paging.next.after` JSON property of a paged response containing more results. (optional)\n * @param string[] $properties A comma separated list of the properties to be returned in the response. If any of the specified properties are not present on the requested object(s), they will be ignored. (optional)\n * @param string[] $associations A comma separated list of object types to retrieve associated IDs for. If any of the specified associations do not exist, they will be ignored. (optional)\n * @param bool $archived Whether to return only results that have been archived. (optional, default to false)\n *\n * @throws \\InvalidArgumentException\n * @return \\GuzzleHttp\\Promise\\PromiseInterface\n */\n public function getPageAsync($limit = 10, $after = null, $properties = null, $associations = null, $archived = false)\n {\n return $this->getPageAsyncWithHttpInfo($limit, $after, $properties, $associations, $archived)\n ->then(\n function ($response) {\n return $response[0];\n }\n );\n }\n\n /**\n * Operation getPageAsyncWithHttpInfo\n *\n * List\n *\n * @param int $limit The maximum number of results to display per page. (optional, default to 10)\n * @param string $after The paging cursor token of the last successfully read resource will be returned as the `paging.next.after` JSON property of a paged response containing more results. (optional)\n * @param string[] $properties A comma separated list of the properties to be returned in the response. If any of the specified properties are not present on the requested object(s), they will be ignored. (optional)\n * @param string[] $associations A comma separated list of object types to retrieve associated IDs for. If any of the specified associations do not exist, they will be ignored. (optional)\n * @param bool $archived Whether to return only results that have been archived. (optional, default to false)\n *\n * @throws \\InvalidArgumentException\n * @return \\GuzzleHttp\\Promise\\PromiseInterface\n */\n public function getPageAsyncWithHttpInfo($limit = 10, $after = null, $properties = null, $associations = null, $archived = false)\n {\n $returnType = '\\HubSpot\\Client\\Crm\\Deals\\Model\\CollectionResponseSimplePublicObjectWithAssociationsForwardPaging';\n $request = $this->getPageRequest($limit, $after, $properties, $associations, $archived);\n\n return $this->client\n ->sendAsync($request, $this->createHttpClientOption())\n ->then(\n function ($response) use ($returnType) {\n if ($returnType === '\\SplFileObject') {\n $content = $response->getBody(); //stream goes to serializer\n } else {\n $content = (string) $response->getBody();\n }\n\n return [\n ObjectSerializer::deserialize($content, $returnType, []),\n $response->getStatusCode(),\n $response->getHeaders()\n ];\n },\n function ($exception) {\n $response = $exception->getResponse();\n $statusCode = $response->getStatusCode();\n throw new ApiException(\n sprintf(\n '[%d] Error connecting to the API (%s)',\n $statusCode,\n $exception->getRequest()->getUri()\n ),\n $statusCode,\n $response->getHeaders(),\n (string) $response->getBody()\n );\n }\n );\n }\n\n /**\n * Create request for operation 'getPage'\n *\n * @param int $limit The maximum number of results to display per page. (optional, default to 10)\n * @param string $after The paging cursor token of the last successfully read resource will be returned as the `paging.next.after` JSON property of a paged response containing more results. (optional)\n * @param string[] $properties A comma separated list of the properties to be returned in the response. If any of the specified properties are not present on the requested object(s), they will be ignored. (optional)\n * @param string[] $associations A comma separated list of object types to retrieve associated IDs for. If any of the specified associations do not exist, they will be ignored. (optional)\n * @param bool $archived Whether to return only results that have been archived. (optional, default to false)\n *\n * @throws \\InvalidArgumentException\n * @return \\GuzzleHttp\\Psr7\\Request\n */\n public function getPageRequest($limit = 10, $after = null, $properties = null, $associations = null, $archived = false)\n {\n\n $resourcePath = '/crm/v3/objects/deals';\n $formParams = [];\n $queryParams = [];\n $headerParams = [];\n $httpBody = '';\n $multipart = false;\n\n // query params\n if ($limit !== null) {\n if('form' === 'form' && is_array($limit)) {\n foreach($limit as $key => $value) {\n $queryParams[$key] = $value;\n }\n }\n else {\n $queryParams['limit'] = $limit;\n }\n }\n // query params\n if ($after !== null) {\n if('form' === 'form' && is_array($after)) {\n foreach($after as $key => $value) {\n $queryParams[$key] = $value;\n }\n }\n else {\n $queryParams['after'] = $after;\n }\n }\n // query params\n if ($properties !== null) {\n if('form' === 'form' && is_array($properties)) {\n foreach($properties as $key => $value) {\n $queryParams[$key] = $value;\n }\n }\n else {\n $queryParams['properties'] = $properties;\n }\n }\n // query params\n if ($associations !== null) {\n if('form' === 'form' && is_array($associations)) {\n foreach($associations as $key => $value) {\n $queryParams[$key] = $value;\n }\n }\n else {\n $queryParams['associations'] = $associations;\n }\n }\n // query params\n if ($archived !== null) {\n if('form' === 'form' && is_array($archived)) {\n foreach($archived as $key => $value) {\n $queryParams[$key] = $value;\n }\n }\n else {\n $queryParams['archived'] = $archived;\n }\n }\n\n\n\n\n if ($multipart) {\n $headers = $this->headerSelector->selectHeadersForMultipart(\n ['application/json', '*/*']\n );\n } else {\n $headers = $this->headerSelector->selectHeaders(\n ['application/json', '*/*'],\n []\n );\n }\n\n // for model (json/xml)\n if (count($formParams) > 0) {\n if ($multipart) {\n $multipartContents = [];\n foreach ($formParams as $formParamName => $formParamValue) {\n $formParamValueItems = is_array($formParamValue) ? $formParamValue : [$formParamValue];\n foreach ($formParamValueItems as $formParamValueItem) {\n $multipartContents[] = [\n 'name' => $formParamName,\n 'contents' => $formParamValueItem\n ];\n }\n }\n // for HTTP post (form)\n $httpBody = new MultipartStream($multipartContents);\n\n } elseif ($headers['Content-Type'] === 'application/json') {\n $httpBody = \\GuzzleHttp\\json_encode($formParams);\n\n } else {\n // for HTTP post (form)\n $httpBody = \\GuzzleHttp\\Psr7\\Query::build($formParams);\n }\n }\n\n // this endpoint requires API key authentication\n $apiKey = $this->config->getApiKeyWithPrefix('hapikey');\n if ($apiKey !== null) {\n $queryParams['hapikey'] = $apiKey;\n }\n // this endpoint requires OAuth (access token)\n if ($this->config->getAccessToken() !== null) {\n $headers['Authorization'] = 'Bearer ' . $this->config->getAccessToken();\n }\n\n $defaultHeaders = [];\n if ($this->config->getUserAgent()) {\n $defaultHeaders['User-Agent'] = $this->config->getUserAgent();\n }\n\n $headers = array_merge(\n $defaultHeaders,\n $headerParams,\n $headers\n );\n\n $query = \\GuzzleHttp\\Psr7\\Query::build($queryParams);\n return new Request(\n 'GET',\n $this->config->getHost() . $resourcePath . ($query ? \"?{$query}\" : ''),\n $headers,\n $httpBody\n );\n }\n\n /**\n * Operation update\n *\n * Update\n *\n * @param string $deal_id deal_id (required)\n * @param \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectInput $simple_public_object_input simple_public_object_input (required)\n * @param string $id_property The name of a property whose values are unique for this object type (optional)\n *\n * @throws \\HubSpot\\Client\\Crm\\Deals\\ApiException on non-2xx response\n * @throws \\InvalidArgumentException\n * @return \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObject|\\HubSpot\\Client\\Crm\\Deals\\Model\\Error\n */\n public function update($deal_id, $simple_public_object_input, $id_property = null)\n {\n list($response) = $this->updateWithHttpInfo($deal_id, $simple_public_object_input, $id_property);\n return $response;\n }\n\n /**\n * Operation updateWithHttpInfo\n *\n * Update\n *\n * @param string $deal_id (required)\n * @param \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectInput $simple_public_object_input (required)\n * @param string $id_property The name of a property whose values are unique for this object type (optional)\n *\n * @throws \\HubSpot\\Client\\Crm\\Deals\\ApiException on non-2xx response\n * @throws \\InvalidArgumentException\n * @return array of \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObject|\\HubSpot\\Client\\Crm\\Deals\\Model\\Error, HTTP status code, HTTP response headers (array of strings)\n */\n public function updateWithHttpInfo($deal_id, $simple_public_object_input, $id_property = null)\n {\n $request = $this->updateRequest($deal_id, $simple_public_object_input, $id_property);\n\n try {\n $options = $this->createHttpClientOption();\n try {\n $response = $this->client->send($request, $options);\n } catch (RequestException $e) {\n throw new ApiException(\n \"[{$e->getCode()}] {$e->getMessage()}\",\n (int) $e->getCode(),\n $e->getResponse() ? $e->getResponse()->getHeaders() : null,\n $e->getResponse() ? (string) $e->getResponse()->getBody() : null\n );\n } catch (ConnectException $e) {\n throw new ApiException(\n \"[{$e->getCode()}] {$e->getMessage()}\",\n (int) $e->getCode(),\n null,\n null\n );\n }\n\n $statusCode = $response->getStatusCode();\n\n if ($statusCode < 200 || $statusCode > 299) {\n throw new ApiException(\n sprintf(\n '[%d] Error connecting to the API (%s)',\n $statusCode,\n (string) $request->getUri()\n ),\n $statusCode,\n $response->getHeaders(),\n (string) $response->getBody()\n );\n }\n\n switch($statusCode) {\n case 200:\n if ('\\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObject' === '\\SplFileObject') {\n $content = $response->getBody(); //stream goes to serializer\n } else {\n $content = (string) $response->getBody();\n }\n\n return [\n ObjectSerializer::deserialize($content, '\\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObject', []),\n $response->getStatusCode(),\n $response->getHeaders()\n ];\n default:\n if ('\\HubSpot\\Client\\Crm\\Deals\\Model\\Error' === '\\SplFileObject') {\n $content = $response->getBody(); //stream goes to serializer\n } else {\n $content = (string) $response->getBody();\n }\n\n return [\n ObjectSerializer::deserialize($content, '\\HubSpot\\Client\\Crm\\Deals\\Model\\Error', []),\n $response->getStatusCode(),\n $response->getHeaders()\n ];\n }\n\n $returnType = '\\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObject';\n if ($returnType === '\\SplFileObject') {\n $content = $response->getBody(); //stream goes to serializer\n } else {\n $content = (string) $response->getBody();\n }\n\n return [\n ObjectSerializer::deserialize($content, $returnType, []),\n $response->getStatusCode(),\n $response->getHeaders()\n ];\n\n } catch (ApiException $e) {\n switch ($e->getCode()) {\n case 200:\n $data = ObjectSerializer::deserialize(\n $e->getResponseBody(),\n '\\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObject',\n $e->getResponseHeaders()\n );\n $e->setResponseObject($data);\n break;\n default:\n $data = ObjectSerializer::deserialize(\n $e->getResponseBody(),\n '\\HubSpot\\Client\\Crm\\Deals\\Model\\Error',\n $e->getResponseHeaders()\n );\n $e->setResponseObject($data);\n break;\n }\n throw $e;\n }\n }\n\n /**\n * Operation updateAsync\n *\n * Update\n *\n * @param string $deal_id (required)\n * @param \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectInput $simple_public_object_input (required)\n * @param string $id_property The name of a property whose values are unique for this object type (optional)\n *\n * @throws \\InvalidArgumentException\n * @return \\GuzzleHttp\\Promise\\PromiseInterface\n */\n public function updateAsync($deal_id, $simple_public_object_input, $id_property = null)\n {\n return $this->updateAsyncWithHttpInfo($deal_id, $simple_public_object_input, $id_property)\n ->then(\n function ($response) {\n return $response[0];\n }\n );\n }\n\n /**\n * Operation updateAsyncWithHttpInfo\n *\n * Update\n *\n * @param string $deal_id (required)\n * @param \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectInput $simple_public_object_input (required)\n * @param string $id_property The name of a property whose values are unique for this object type (optional)\n *\n * @throws \\InvalidArgumentException\n * @return \\GuzzleHttp\\Promise\\PromiseInterface\n */\n public function updateAsyncWithHttpInfo($deal_id, $simple_public_object_input, $id_property = null)\n {\n $returnType = '\\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObject';\n $request = $this->updateRequest($deal_id, $simple_public_object_input, $id_property);\n\n return $this->client\n ->sendAsync($request, $this->createHttpClientOption())\n ->then(\n function ($response) use ($returnType) {\n if ($returnType === '\\SplFileObject') {\n $content = $response->getBody(); //stream goes to serializer\n } else {\n $content = (string) $response->getBody();\n }\n\n return [\n ObjectSerializer::deserialize($content, $returnType, []),\n $response->getStatusCode(),\n $response->getHeaders()\n ];\n },\n function ($exception) {\n $response = $exception->getResponse();\n $statusCode = $response->getStatusCode();\n throw new ApiException(\n sprintf(\n '[%d] Error connecting to the API (%s)',\n $statusCode,\n $exception->getRequest()->getUri()\n ),\n $statusCode,\n $response->getHeaders(),\n (string) $response->getBody()\n );\n }\n );\n }\n\n /**\n * Create request for operation 'update'\n *\n * @param string $deal_id (required)\n * @param \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectInput $simple_public_object_input (required)\n * @param string $id_property The name of a property whose values are unique for this object type (optional)\n *\n * @throws \\InvalidArgumentException\n * @return \\GuzzleHttp\\Psr7\\Request\n */\n public function updateRequest($deal_id, $simple_public_object_input, $id_property = null)\n {\n // verify the required parameter 'deal_id' is set\n if ($deal_id === null || (is_array($deal_id) && count($deal_id) === 0)) {\n throw new \\InvalidArgumentException(\n 'Missing the required parameter $deal_id when calling update'\n );\n }\n // verify the required parameter 'simple_public_object_input' is set\n if ($simple_public_object_input === null || (is_array($simple_public_object_input) && count($simple_public_object_input) === 0)) {\n throw new \\InvalidArgumentException(\n 'Missing the required parameter $simple_public_object_input when calling update'\n );\n }\n\n $resourcePath = '/crm/v3/objects/deals/{dealId}';\n $formParams = [];\n $queryParams = [];\n $headerParams = [];\n $httpBody = '';\n $multipart = false;\n\n // query params\n if ($id_property !== null) {\n if('form' === 'form' && is_array($id_property)) {\n foreach($id_property as $key => $value) {\n $queryParams[$key] = $value;\n }\n }\n else {\n $queryParams['idProperty'] = $id_property;\n }\n }\n\n\n // path params\n if ($deal_id !== null) {\n $resourcePath = str_replace(\n '{' . 'dealId' . '}',\n ObjectSerializer::toPathValue($deal_id),\n $resourcePath\n );\n }\n\n\n if ($multipart) {\n $headers = $this->headerSelector->selectHeadersForMultipart(\n ['application/json', '*/*']\n );\n } else {\n $headers = $this->headerSelector->selectHeaders(\n ['application/json', '*/*'],\n ['application/json']\n );\n }\n\n // for model (json/xml)\n if (isset($simple_public_object_input)) {\n if ($headers['Content-Type'] === 'application/json') {\n $httpBody = \\GuzzleHttp\\json_encode(ObjectSerializer::sanitizeForSerialization($simple_public_object_input));\n } else {\n $httpBody = $simple_public_object_input;\n }\n } elseif (count($formParams) > 0) {\n if ($multipart) {\n $multipartContents = [];\n foreach ($formParams as $formParamName => $formParamValue) {\n $formParamValueItems = is_array($formParamValue) ? $formParamValue : [$formParamValue];\n foreach ($formParamValueItems as $formParamValueItem) {\n $multipartContents[] = [\n 'name' => $formParamName,\n 'contents' => $formParamValueItem\n ];\n }\n }\n // for HTTP post (form)\n $httpBody = new MultipartStream($multipartContents);\n\n } elseif ($headers['Content-Type'] === 'application/json') {\n $httpBody = \\GuzzleHttp\\json_encode($formParams);\n\n } else {\n // for HTTP post (form)\n $httpBody = \\GuzzleHttp\\Psr7\\Query::build($formParams);\n }\n }\n\n // this endpoint requires API key authentication\n $apiKey = $this->config->getApiKeyWithPrefix('hapikey');\n if ($apiKey !== null) {\n $queryParams['hapikey'] = $apiKey;\n }\n // this endpoint requires OAuth (access token)\n if ($this->config->getAccessToken() !== null) {\n $headers['Authorization'] = 'Bearer ' . $this->config->getAccessToken();\n }\n\n $defaultHeaders = [];\n if ($this->config->getUserAgent()) {\n $defaultHeaders['User-Agent'] = $this->config->getUserAgent();\n }\n\n $headers = array_merge(\n $defaultHeaders,\n $headerParams,\n $headers\n );\n\n $query = \\GuzzleHttp\\Psr7\\Query::build($queryParams);\n return new Request(\n 'PATCH',\n $this->config->getHost() . $resourcePath . ($query ? \"?{$query}\" : ''),\n $headers,\n $httpBody\n );\n }\n\n /**\n * Create http client option\n *\n * @throws \\RuntimeException on file opening failure\n * @return array of http client options\n */\n protected function createHttpClientOption()\n {\n $options = [];\n if ($this->config->getDebug()) {\n $options[RequestOptions::DEBUG] = fopen($this->config->getDebugFile(), 'a');\n if (!$options[RequestOptions::DEBUG]) {\n throw new \\RuntimeException('Failed to open the debug file: ' . $this->config->getDebugFile());\n }\n }\n\n return $options;\n }\n}","depth":4,"on_screen":true,"value":"<?php\n/**\n * BasicApi\n * PHP version 7.3\n *\n * @category Class\n * @package HubSpot\\Client\\Crm\\Deals\n * @author OpenAPI Generator team\n * @link https://openapi-generator.tech\n */\n\n/**\n * Deals\n *\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: v3\n * Generated by: https://openapi-generator.tech\n * OpenAPI Generator version: 5.3.0\n */\n\n/**\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\nnamespace HubSpot\\Client\\Crm\\Deals\\Api;\n\nuse GuzzleHttp\\Client;\nuse GuzzleHttp\\ClientInterface;\nuse GuzzleHttp\\Exception\\RequestException;\nuse GuzzleHttp\\Exception\\ConnectException;\nuse GuzzleHttp\\Psr7\\MultipartStream;\nuse GuzzleHttp\\Psr7\\Request;\nuse GuzzleHttp\\RequestOptions;\nuse HubSpot\\Client\\Crm\\Deals\\ApiException;\nuse HubSpot\\Client\\Crm\\Deals\\Configuration;\nuse HubSpot\\Client\\Crm\\Deals\\HeaderSelector;\nuse HubSpot\\Client\\Crm\\Deals\\ObjectSerializer;\n\n/**\n * BasicApi Class Doc Comment\n *\n * @category Class\n * @package HubSpot\\Client\\Crm\\Deals\n * @author OpenAPI Generator team\n * @link https://openapi-generator.tech\n */\nclass BasicApi\n{\n /**\n * @var ClientInterface\n */\n protected $client;\n\n /**\n * @var Configuration\n */\n protected $config;\n\n /**\n * @var HeaderSelector\n */\n protected $headerSelector;\n\n /**\n * @var int Host index\n */\n protected $hostIndex;\n\n /**\n * @param ClientInterface $client\n * @param Configuration $config\n * @param HeaderSelector $selector\n * @param int $hostIndex (Optional) host index to select the list of hosts if defined in the OpenAPI spec\n */\n public function __construct(\n ClientInterface $client = null,\n Configuration $config = null,\n HeaderSelector $selector = null,\n $hostIndex = 0\n ) {\n $this->client = $client ?: new Client();\n $this->config = $config ?: new Configuration();\n $this->headerSelector = $selector ?: new HeaderSelector();\n $this->hostIndex = $hostIndex;\n }\n\n /**\n * Set the host index\n *\n * @param int $hostIndex Host index (required)\n */\n public function setHostIndex($hostIndex): void\n {\n $this->hostIndex = $hostIndex;\n }\n\n /**\n * Get the host index\n *\n * @return int Host index\n */\n public function getHostIndex()\n {\n return $this->hostIndex;\n }\n\n /**\n * @return Configuration\n */\n public function getConfig()\n {\n return $this->config;\n }\n\n /**\n * Operation archive\n *\n * Archive\n *\n * @param string $deal_id deal_id (required)\n *\n * @throws \\HubSpot\\Client\\Crm\\Deals\\ApiException on non-2xx response\n * @throws \\InvalidArgumentException\n * @return void\n */\n public function archive($deal_id)\n {\n $this->archiveWithHttpInfo($deal_id);\n }\n\n /**\n * Operation archiveWithHttpInfo\n *\n * Archive\n *\n * @param string $deal_id (required)\n *\n * @throws \\HubSpot\\Client\\Crm\\Deals\\ApiException on non-2xx response\n * @throws \\InvalidArgumentException\n * @return array of null, HTTP status code, HTTP response headers (array of strings)\n */\n public function archiveWithHttpInfo($deal_id)\n {\n $request = $this->archiveRequest($deal_id);\n\n try {\n $options = $this->createHttpClientOption();\n try {\n $response = $this->client->send($request, $options);\n } catch (RequestException $e) {\n throw new ApiException(\n \"[{$e->getCode()}] {$e->getMessage()}\",\n (int) $e->getCode(),\n $e->getResponse() ? $e->getResponse()->getHeaders() : null,\n $e->getResponse() ? (string) $e->getResponse()->getBody() : null\n );\n } catch (ConnectException $e) {\n throw new ApiException(\n \"[{$e->getCode()}] {$e->getMessage()}\",\n (int) $e->getCode(),\n null,\n null\n );\n }\n\n $statusCode = $response->getStatusCode();\n\n if ($statusCode < 200 || $statusCode > 299) {\n throw new ApiException(\n sprintf(\n '[%d] Error connecting to the API (%s)',\n $statusCode,\n (string) $request->getUri()\n ),\n $statusCode,\n $response->getHeaders(),\n (string) $response->getBody()\n );\n }\n\n return [null, $statusCode, $response->getHeaders()];\n\n } catch (ApiException $e) {\n switch ($e->getCode()) {\n default:\n $data = ObjectSerializer::deserialize(\n $e->getResponseBody(),\n '\\HubSpot\\Client\\Crm\\Deals\\Model\\Error',\n $e->getResponseHeaders()\n );\n $e->setResponseObject($data);\n break;\n }\n throw $e;\n }\n }\n\n /**\n * Operation archiveAsync\n *\n * Archive\n *\n * @param string $deal_id (required)\n *\n * @throws \\InvalidArgumentException\n * @return \\GuzzleHttp\\Promise\\PromiseInterface\n */\n public function archiveAsync($deal_id)\n {\n return $this->archiveAsyncWithHttpInfo($deal_id)\n ->then(\n function ($response) {\n return $response[0];\n }\n );\n }\n\n /**\n * Operation archiveAsyncWithHttpInfo\n *\n * Archive\n *\n * @param string $deal_id (required)\n *\n * @throws \\InvalidArgumentException\n * @return \\GuzzleHttp\\Promise\\PromiseInterface\n */\n public function archiveAsyncWithHttpInfo($deal_id)\n {\n $returnType = '';\n $request = $this->archiveRequest($deal_id);\n\n return $this->client\n ->sendAsync($request, $this->createHttpClientOption())\n ->then(\n function ($response) use ($returnType) {\n return [null, $response->getStatusCode(), $response->getHeaders()];\n },\n function ($exception) {\n $response = $exception->getResponse();\n $statusCode = $response->getStatusCode();\n throw new ApiException(\n sprintf(\n '[%d] Error connecting to the API (%s)',\n $statusCode,\n $exception->getRequest()->getUri()\n ),\n $statusCode,\n $response->getHeaders(),\n (string) $response->getBody()\n );\n }\n );\n }\n\n /**\n * Create request for operation 'archive'\n *\n * @param string $deal_id (required)\n *\n * @throws \\InvalidArgumentException\n * @return \\GuzzleHttp\\Psr7\\Request\n */\n public function archiveRequest($deal_id)\n {\n // verify the required parameter 'deal_id' is set\n if ($deal_id === null || (is_array($deal_id) && count($deal_id) === 0)) {\n throw new \\InvalidArgumentException(\n 'Missing the required parameter $deal_id when calling archive'\n );\n }\n\n $resourcePath = '/crm/v3/objects/deals/{dealId}';\n $formParams = [];\n $queryParams = [];\n $headerParams = [];\n $httpBody = '';\n $multipart = false;\n\n\n\n // path params\n if ($deal_id !== null) {\n $resourcePath = str_replace(\n '{' . 'dealId' . '}',\n ObjectSerializer::toPathValue($deal_id),\n $resourcePath\n );\n }\n\n\n if ($multipart) {\n $headers = $this->headerSelector->selectHeadersForMultipart(\n ['*/*']\n );\n } else {\n $headers = $this->headerSelector->selectHeaders(\n ['*/*'],\n []\n );\n }\n\n // for model (json/xml)\n if (count($formParams) > 0) {\n if ($multipart) {\n $multipartContents = [];\n foreach ($formParams as $formParamName => $formParamValue) {\n $formParamValueItems = is_array($formParamValue) ? $formParamValue : [$formParamValue];\n foreach ($formParamValueItems as $formParamValueItem) {\n $multipartContents[] = [\n 'name' => $formParamName,\n 'contents' => $formParamValueItem\n ];\n }\n }\n // for HTTP post (form)\n $httpBody = new MultipartStream($multipartContents);\n\n } elseif ($headers['Content-Type'] === 'application/json') {\n $httpBody = \\GuzzleHttp\\json_encode($formParams);\n\n } else {\n // for HTTP post (form)\n $httpBody = \\GuzzleHttp\\Psr7\\Query::build($formParams);\n }\n }\n\n // this endpoint requires API key authentication\n $apiKey = $this->config->getApiKeyWithPrefix('hapikey');\n if ($apiKey !== null) {\n $queryParams['hapikey'] = $apiKey;\n }\n // this endpoint requires OAuth (access token)\n if ($this->config->getAccessToken() !== null) {\n $headers['Authorization'] = 'Bearer ' . $this->config->getAccessToken();\n }\n\n $defaultHeaders = [];\n if ($this->config->getUserAgent()) {\n $defaultHeaders['User-Agent'] = $this->config->getUserAgent();\n }\n\n $headers = array_merge(\n $defaultHeaders,\n $headerParams,\n $headers\n );\n\n $query = \\GuzzleHttp\\Psr7\\Query::build($queryParams);\n return new Request(\n 'DELETE',\n $this->config->getHost() . $resourcePath . ($query ? \"?{$query}\" : ''),\n $headers,\n $httpBody\n );\n }\n\n /**\n * Operation create\n *\n * Create\n *\n * @param \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectInput $simple_public_object_input simple_public_object_input (required)\n *\n * @throws \\HubSpot\\Client\\Crm\\Deals\\ApiException on non-2xx response\n * @throws \\InvalidArgumentException\n * @return \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObject|\\HubSpot\\Client\\Crm\\Deals\\Model\\Error\n */\n public function create($simple_public_object_input)\n {\n list($response) = $this->createWithHttpInfo($simple_public_object_input);\n return $response;\n }\n\n /**\n * Operation createWithHttpInfo\n *\n * Create\n *\n * @param \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectInput $simple_public_object_input (required)\n *\n * @throws \\HubSpot\\Client\\Crm\\Deals\\ApiException on non-2xx response\n * @throws \\InvalidArgumentException\n * @return array of \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObject|\\HubSpot\\Client\\Crm\\Deals\\Model\\Error, HTTP status code, HTTP response headers (array of strings)\n */\n public function createWithHttpInfo($simple_public_object_input)\n {\n $request = $this->createRequest($simple_public_object_input);\n\n try {\n $options = $this->createHttpClientOption();\n try {\n $response = $this->client->send($request, $options);\n } catch (RequestException $e) {\n throw new ApiException(\n \"[{$e->getCode()}] {$e->getMessage()}\",\n (int) $e->getCode(),\n $e->getResponse() ? $e->getResponse()->getHeaders() : null,\n $e->getResponse() ? (string) $e->getResponse()->getBody() : null\n );\n } catch (ConnectException $e) {\n throw new ApiException(\n \"[{$e->getCode()}] {$e->getMessage()}\",\n (int) $e->getCode(),\n null,\n null\n );\n }\n\n $statusCode = $response->getStatusCode();\n\n if ($statusCode < 200 || $statusCode > 299) {\n throw new ApiException(\n sprintf(\n '[%d] Error connecting to the API (%s)',\n $statusCode,\n (string) $request->getUri()\n ),\n $statusCode,\n $response->getHeaders(),\n (string) $response->getBody()\n );\n }\n\n switch($statusCode) {\n case 201:\n if ('\\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObject' === '\\SplFileObject') {\n $content = $response->getBody(); //stream goes to serializer\n } else {\n $content = (string) $response->getBody();\n }\n\n return [\n ObjectSerializer::deserialize($content, '\\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObject', []),\n $response->getStatusCode(),\n $response->getHeaders()\n ];\n default:\n if ('\\HubSpot\\Client\\Crm\\Deals\\Model\\Error' === '\\SplFileObject') {\n $content = $response->getBody(); //stream goes to serializer\n } else {\n $content = (string) $response->getBody();\n }\n\n return [\n ObjectSerializer::deserialize($content, '\\HubSpot\\Client\\Crm\\Deals\\Model\\Error', []),\n $response->getStatusCode(),\n $response->getHeaders()\n ];\n }\n\n $returnType = '\\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObject';\n if ($returnType === '\\SplFileObject') {\n $content = $response->getBody(); //stream goes to serializer\n } else {\n $content = (string) $response->getBody();\n }\n\n return [\n ObjectSerializer::deserialize($content, $returnType, []),\n $response->getStatusCode(),\n $response->getHeaders()\n ];\n\n } catch (ApiException $e) {\n switch ($e->getCode()) {\n case 201:\n $data = ObjectSerializer::deserialize(\n $e->getResponseBody(),\n '\\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObject',\n $e->getResponseHeaders()\n );\n $e->setResponseObject($data);\n break;\n default:\n $data = ObjectSerializer::deserialize(\n $e->getResponseBody(),\n '\\HubSpot\\Client\\Crm\\Deals\\Model\\Error',\n $e->getResponseHeaders()\n );\n $e->setResponseObject($data);\n break;\n }\n throw $e;\n }\n }\n\n /**\n * Operation createAsync\n *\n * Create\n *\n * @param \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectInput $simple_public_object_input (required)\n *\n * @throws \\InvalidArgumentException\n * @return \\GuzzleHttp\\Promise\\PromiseInterface\n */\n public function createAsync($simple_public_object_input)\n {\n return $this->createAsyncWithHttpInfo($simple_public_object_input)\n ->then(\n function ($response) {\n return $response[0];\n }\n );\n }\n\n /**\n * Operation createAsyncWithHttpInfo\n *\n * Create\n *\n * @param \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectInput $simple_public_object_input (required)\n *\n * @throws \\InvalidArgumentException\n * @return \\GuzzleHttp\\Promise\\PromiseInterface\n */\n public function createAsyncWithHttpInfo($simple_public_object_input)\n {\n $returnType = '\\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObject';\n $request = $this->createRequest($simple_public_object_input);\n\n return $this->client\n ->sendAsync($request, $this->createHttpClientOption())\n ->then(\n function ($response) use ($returnType) {\n if ($returnType === '\\SplFileObject') {\n $content = $response->getBody(); //stream goes to serializer\n } else {\n $content = (string) $response->getBody();\n }\n\n return [\n ObjectSerializer::deserialize($content, $returnType, []),\n $response->getStatusCode(),\n $response->getHeaders()\n ];\n },\n function ($exception) {\n $response = $exception->getResponse();\n $statusCode = $response->getStatusCode();\n throw new ApiException(\n sprintf(\n '[%d] Error connecting to the API (%s)',\n $statusCode,\n $exception->getRequest()->getUri()\n ),\n $statusCode,\n $response->getHeaders(),\n (string) $response->getBody()\n );\n }\n );\n }\n\n /**\n * Create request for operation 'create'\n *\n * @param \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectInput $simple_public_object_input (required)\n *\n * @throws \\InvalidArgumentException\n * @return \\GuzzleHttp\\Psr7\\Request\n */\n public function createRequest($simple_public_object_input)\n {\n // verify the required parameter 'simple_public_object_input' is set\n if ($simple_public_object_input === null || (is_array($simple_public_object_input) && count($simple_public_object_input) === 0)) {\n throw new \\InvalidArgumentException(\n 'Missing the required parameter $simple_public_object_input when calling create'\n );\n }\n\n $resourcePath = '/crm/v3/objects/deals';\n $formParams = [];\n $queryParams = [];\n $headerParams = [];\n $httpBody = '';\n $multipart = false;\n\n\n\n\n\n if ($multipart) {\n $headers = $this->headerSelector->selectHeadersForMultipart(\n ['application/json', '*/*']\n );\n } else {\n $headers = $this->headerSelector->selectHeaders(\n ['application/json', '*/*'],\n ['application/json']\n );\n }\n\n // for model (json/xml)\n if (isset($simple_public_object_input)) {\n if ($headers['Content-Type'] === 'application/json') {\n $httpBody = \\GuzzleHttp\\json_encode(ObjectSerializer::sanitizeForSerialization($simple_public_object_input));\n } else {\n $httpBody = $simple_public_object_input;\n }\n } elseif (count($formParams) > 0) {\n if ($multipart) {\n $multipartContents = [];\n foreach ($formParams as $formParamName => $formParamValue) {\n $formParamValueItems = is_array($formParamValue) ? $formParamValue : [$formParamValue];\n foreach ($formParamValueItems as $formParamValueItem) {\n $multipartContents[] = [\n 'name' => $formParamName,\n 'contents' => $formParamValueItem\n ];\n }\n }\n // for HTTP post (form)\n $httpBody = new MultipartStream($multipartContents);\n\n } elseif ($headers['Content-Type'] === 'application/json') {\n $httpBody = \\GuzzleHttp\\json_encode($formParams);\n\n } else {\n // for HTTP post (form)\n $httpBody = \\GuzzleHttp\\Psr7\\Query::build($formParams);\n }\n }\n\n // this endpoint requires API key authentication\n $apiKey = $this->config->getApiKeyWithPrefix('hapikey');\n if ($apiKey !== null) {\n $queryParams['hapikey'] = $apiKey;\n }\n // this endpoint requires OAuth (access token)\n if ($this->config->getAccessToken() !== null) {\n $headers['Authorization'] = 'Bearer ' . $this->config->getAccessToken();\n }\n\n $defaultHeaders = [];\n if ($this->config->getUserAgent()) {\n $defaultHeaders['User-Agent'] = $this->config->getUserAgent();\n }\n\n $headers = array_merge(\n $defaultHeaders,\n $headerParams,\n $headers\n );\n\n $query = \\GuzzleHttp\\Psr7\\Query::build($queryParams);\n return new Request(\n 'POST',\n $this->config->getHost() . $resourcePath . ($query ? \"?{$query}\" : ''),\n $headers,\n $httpBody\n );\n }\n\n /**\n * Operation getById\n *\n * Read\n *\n * @param string $deal_id deal_id (required)\n * @param string[] $properties A comma separated list of the properties to be returned in the response. If any of the specified properties are not present on the requested object(s), they will be ignored. (optional)\n * @param string[] $associations A comma separated list of object types to retrieve associated IDs for. If any of the specified associations do not exist, they will be ignored. (optional)\n * @param bool $archived Whether to return only results that have been archived. (optional, default to false)\n * @param string $id_property The name of a property whose values are unique for this object type (optional)\n *\n * @throws \\HubSpot\\Client\\Crm\\Deals\\ApiException on non-2xx response\n * @throws \\InvalidArgumentException\n * @return \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations|\\HubSpot\\Client\\Crm\\Deals\\Model\\Error\n */\n public function getById($deal_id, $properties = null, $associations = null, $archived = false, $id_property = null)\n {\n list($response) = $this->getByIdWithHttpInfo($deal_id, $properties, $associations, $archived, $id_property);\n return $response;\n }\n\n /**\n * Operation getByIdWithHttpInfo\n *\n * Read\n *\n * @param string $deal_id (required)\n * @param string[] $properties A comma separated list of the properties to be returned in the response. If any of the specified properties are not present on the requested object(s), they will be ignored. (optional)\n * @param string[] $associations A comma separated list of object types to retrieve associated IDs for. If any of the specified associations do not exist, they will be ignored. (optional)\n * @param bool $archived Whether to return only results that have been archived. (optional, default to false)\n * @param string $id_property The name of a property whose values are unique for this object type (optional)\n *\n * @throws \\HubSpot\\Client\\Crm\\Deals\\ApiException on non-2xx response\n * @throws \\InvalidArgumentException\n * @return array of \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations|\\HubSpot\\Client\\Crm\\Deals\\Model\\Error, HTTP status code, HTTP response headers (array of strings)\n */\n public function getByIdWithHttpInfo($deal_id, $properties = null, $associations = null, $archived = false, $id_property = null)\n {\n $request = $this->getByIdRequest($deal_id, $properties, $associations, $archived, $id_property);\n\n try {\n $options = $this->createHttpClientOption();\n try {\n $response = $this->client->send($request, $options);\n } catch (RequestException $e) {\n throw new ApiException(\n \"[{$e->getCode()}] {$e->getMessage()}\",\n (int) $e->getCode(),\n $e->getResponse() ? $e->getResponse()->getHeaders() : null,\n $e->getResponse() ? (string) $e->getResponse()->getBody() : null\n );\n } catch (ConnectException $e) {\n throw new ApiException(\n \"[{$e->getCode()}] {$e->getMessage()}\",\n (int) $e->getCode(),\n null,\n null\n );\n }\n\n $statusCode = $response->getStatusCode();\n\n if ($statusCode < 200 || $statusCode > 299) {\n throw new ApiException(\n sprintf(\n '[%d] Error connecting to the API (%s)',\n $statusCode,\n (string) $request->getUri()\n ),\n $statusCode,\n $response->getHeaders(),\n (string) $response->getBody()\n );\n }\n\n switch($statusCode) {\n case 200:\n if ('\\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations' === '\\SplFileObject') {\n $content = $response->getBody(); //stream goes to serializer\n } else {\n $content = (string) $response->getBody();\n }\n\n return [\n ObjectSerializer::deserialize($content, '\\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations', []),\n $response->getStatusCode(),\n $response->getHeaders()\n ];\n default:\n if ('\\HubSpot\\Client\\Crm\\Deals\\Model\\Error' === '\\SplFileObject') {\n $content = $response->getBody(); //stream goes to serializer\n } else {\n $content = (string) $response->getBody();\n }\n\n return [\n ObjectSerializer::deserialize($content, '\\HubSpot\\Client\\Crm\\Deals\\Model\\Error', []),\n $response->getStatusCode(),\n $response->getHeaders()\n ];\n }\n\n $returnType = '\\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations';\n if ($returnType === '\\SplFileObject') {\n $content = $response->getBody(); //stream goes to serializer\n } else {\n $content = (string) $response->getBody();\n }\n\n return [\n ObjectSerializer::deserialize($content, $returnType, []),\n $response->getStatusCode(),\n $response->getHeaders()\n ];\n\n } catch (ApiException $e) {\n switch ($e->getCode()) {\n case 200:\n $data = ObjectSerializer::deserialize(\n $e->getResponseBody(),\n '\\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations',\n $e->getResponseHeaders()\n );\n $e->setResponseObject($data);\n break;\n default:\n $data = ObjectSerializer::deserialize(\n $e->getResponseBody(),\n '\\HubSpot\\Client\\Crm\\Deals\\Model\\Error',\n $e->getResponseHeaders()\n );\n $e->setResponseObject($data);\n break;\n }\n throw $e;\n }\n }\n\n /**\n * Operation getByIdAsync\n *\n * Read\n *\n * @param string $deal_id (required)\n * @param string[] $properties A comma separated list of the properties to be returned in the response. If any of the specified properties are not present on the requested object(s), they will be ignored. (optional)\n * @param string[] $associations A comma separated list of object types to retrieve associated IDs for. If any of the specified associations do not exist, they will be ignored. (optional)\n * @param bool $archived Whether to return only results that have been archived. (optional, default to false)\n * @param string $id_property The name of a property whose values are unique for this object type (optional)\n *\n * @throws \\InvalidArgumentException\n * @return \\GuzzleHttp\\Promise\\PromiseInterface\n */\n public function getByIdAsync($deal_id, $properties = null, $associations = null, $archived = false, $id_property = null)\n {\n return $this->getByIdAsyncWithHttpInfo($deal_id, $properties, $associations, $archived, $id_property)\n ->then(\n function ($response) {\n return $response[0];\n }\n );\n }\n\n /**\n * Operation getByIdAsyncWithHttpInfo\n *\n * Read\n *\n * @param string $deal_id (required)\n * @param string[] $properties A comma separated list of the properties to be returned in the response. If any of the specified properties are not present on the requested object(s), they will be ignored. (optional)\n * @param string[] $associations A comma separated list of object types to retrieve associated IDs for. If any of the specified associations do not exist, they will be ignored. (optional)\n * @param bool $archived Whether to return only results that have been archived. (optional, default to false)\n * @param string $id_property The name of a property whose values are unique for this object type (optional)\n *\n * @throws \\InvalidArgumentException\n * @return \\GuzzleHttp\\Promise\\PromiseInterface\n */\n public function getByIdAsyncWithHttpInfo($deal_id, $properties = null, $associations = null, $archived = false, $id_property = null)\n {\n $returnType = '\\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations';\n $request = $this->getByIdRequest($deal_id, $properties, $associations, $archived, $id_property);\n\n return $this->client\n ->sendAsync($request, $this->createHttpClientOption())\n ->then(\n function ($response) use ($returnType) {\n if ($returnType === '\\SplFileObject') {\n $content = $response->getBody(); //stream goes to serializer\n } else {\n $content = (string) $response->getBody();\n }\n\n return [\n ObjectSerializer::deserialize($content, $returnType, []),\n $response->getStatusCode(),\n $response->getHeaders()\n ];\n },\n function ($exception) {\n $response = $exception->getResponse();\n $statusCode = $response->getStatusCode();\n throw new ApiException(\n sprintf(\n '[%d] Error connecting to the API (%s)',\n $statusCode,\n $exception->getRequest()->getUri()\n ),\n $statusCode,\n $response->getHeaders(),\n (string) $response->getBody()\n );\n }\n );\n }\n\n /**\n * Create request for operation 'getById'\n *\n * @param string $deal_id (required)\n * @param string[] $properties A comma separated list of the properties to be returned in the response. If any of the specified properties are not present on the requested object(s), they will be ignored. (optional)\n * @param string[] $associations A comma separated list of object types to retrieve associated IDs for. If any of the specified associations do not exist, they will be ignored. (optional)\n * @param bool $archived Whether to return only results that have been archived. (optional, default to false)\n * @param string $id_property The name of a property whose values are unique for this object type (optional)\n *\n * @throws \\InvalidArgumentException\n * @return \\GuzzleHttp\\Psr7\\Request\n */\n public function getByIdRequest($deal_id, $properties = null, $associations = null, $archived = false, $id_property = null)\n {\n // verify the required parameter 'deal_id' is set\n if ($deal_id === null || (is_array($deal_id) && count($deal_id) === 0)) {\n throw new \\InvalidArgumentException(\n 'Missing the required parameter $deal_id when calling getById'\n );\n }\n\n $resourcePath = '/crm/v3/objects/deals/{dealId}';\n $formParams = [];\n $queryParams = [];\n $headerParams = [];\n $httpBody = '';\n $multipart = false;\n\n // query params\n if ($properties !== null) {\n if('form' === 'form' && is_array($properties)) {\n foreach($properties as $key => $value) {\n $queryParams[$key] = $value;\n }\n }\n else {\n $queryParams['properties'] = $properties;\n }\n }\n // query params\n if ($associations !== null) {\n if('form' === 'form' && is_array($associations)) {\n foreach($associations as $key => $value) {\n $queryParams[$key] = $value;\n }\n }\n else {\n $queryParams['associations'] = $associations;\n }\n }\n // query params\n if ($archived !== null) {\n if('form' === 'form' && is_array($archived)) {\n foreach($archived as $key => $value) {\n $queryParams[$key] = $value;\n }\n }\n else {\n $queryParams['archived'] = $archived;\n }\n }\n // query params\n if ($id_property !== null) {\n if('form' === 'form' && is_array($id_property)) {\n foreach($id_property as $key => $value) {\n $queryParams[$key] = $value;\n }\n }\n else {\n $queryParams['idProperty'] = $id_property;\n }\n }\n\n\n // path params\n if ($deal_id !== null) {\n $resourcePath = str_replace(\n '{' . 'dealId' . '}',\n ObjectSerializer::toPathValue($deal_id),\n $resourcePath\n );\n }\n\n\n if ($multipart) {\n $headers = $this->headerSelector->selectHeadersForMultipart(\n ['application/json', '*/*']\n );\n } else {\n $headers = $this->headerSelector->selectHeaders(\n ['application/json', '*/*'],\n []\n );\n }\n\n // for model (json/xml)\n if (count($formParams) > 0) {\n if ($multipart) {\n $multipartContents = [];\n foreach ($formParams as $formParamName => $formParamValue) {\n $formParamValueItems = is_array($formParamValue) ? $formParamValue : [$formParamValue];\n foreach ($formParamValueItems as $formParamValueItem) {\n $multipartContents[] = [\n 'name' => $formParamName,\n 'contents' => $formParamValueItem\n ];\n }\n }\n // for HTTP post (form)\n $httpBody = new MultipartStream($multipartContents);\n\n } elseif ($headers['Content-Type'] === 'application/json') {\n $httpBody = \\GuzzleHttp\\json_encode($formParams);\n\n } else {\n // for HTTP post (form)\n $httpBody = \\GuzzleHttp\\Psr7\\Query::build($formParams);\n }\n }\n\n // this endpoint requires API key authentication\n $apiKey = $this->config->getApiKeyWithPrefix('hapikey');\n if ($apiKey !== null) {\n $queryParams['hapikey'] = $apiKey;\n }\n // this endpoint requires OAuth (access token)\n if ($this->config->getAccessToken() !== null) {\n $headers['Authorization'] = 'Bearer ' . $this->config->getAccessToken();\n }\n\n $defaultHeaders = [];\n if ($this->config->getUserAgent()) {\n $defaultHeaders['User-Agent'] = $this->config->getUserAgent();\n }\n\n $headers = array_merge(\n $defaultHeaders,\n $headerParams,\n $headers\n );\n\n $query = \\GuzzleHttp\\Psr7\\Query::build($queryParams);\n return new Request(\n 'GET',\n $this->config->getHost() . $resourcePath . ($query ? \"?{$query}\" : ''),\n $headers,\n $httpBody\n );\n }\n\n /**\n * Operation getPage\n *\n * List\n *\n * @param int $limit The maximum number of results to display per page. (optional, default to 10)\n * @param string $after The paging cursor token of the last successfully read resource will be returned as the `paging.next.after` JSON property of a paged response containing more results. (optional)\n * @param string[] $properties A comma separated list of the properties to be returned in the response. If any of the specified properties are not present on the requested object(s), they will be ignored. (optional)\n * @param string[] $associations A comma separated list of object types to retrieve associated IDs for. If any of the specified associations do not exist, they will be ignored. (optional)\n * @param bool $archived Whether to return only results that have been archived. (optional, default to false)\n *\n * @throws \\HubSpot\\Client\\Crm\\Deals\\ApiException on non-2xx response\n * @throws \\InvalidArgumentException\n * @return \\HubSpot\\Client\\Crm\\Deals\\Model\\CollectionResponseSimplePublicObjectWithAssociationsForwardPaging|\\HubSpot\\Client\\Crm\\Deals\\Model\\Error\n */\n public function getPage($limit = 10, $after = null, $properties = null, $associations = null, $archived = false)\n {\n list($response) = $this->getPageWithHttpInfo($limit, $after, $properties, $associations, $archived);\n return $response;\n }\n\n /**\n * Operation getPageWithHttpInfo\n *\n * List\n *\n * @param int $limit The maximum number of results to display per page. (optional, default to 10)\n * @param string $after The paging cursor token of the last successfully read resource will be returned as the `paging.next.after` JSON property of a paged response containing more results. (optional)\n * @param string[] $properties A comma separated list of the properties to be returned in the response. If any of the specified properties are not present on the requested object(s), they will be ignored. (optional)\n * @param string[] $associations A comma separated list of object types to retrieve associated IDs for. If any of the specified associations do not exist, they will be ignored. (optional)\n * @param bool $archived Whether to return only results that have been archived. (optional, default to false)\n *\n * @throws \\HubSpot\\Client\\Crm\\Deals\\ApiException on non-2xx response\n * @throws \\InvalidArgumentException\n * @return array of \\HubSpot\\Client\\Crm\\Deals\\Model\\CollectionResponseSimplePublicObjectWithAssociationsForwardPaging|\\HubSpot\\Client\\Crm\\Deals\\Model\\Error, HTTP status code, HTTP response headers (array of strings)\n */\n public function getPageWithHttpInfo($limit = 10, $after = null, $properties = null, $associations = null, $archived = false)\n {\n $request = $this->getPageRequest($limit, $after, $properties, $associations, $archived);\n\n try {\n $options = $this->createHttpClientOption();\n try {\n $response = $this->client->send($request, $options);\n } catch (RequestException $e) {\n throw new ApiException(\n \"[{$e->getCode()}] {$e->getMessage()}\",\n (int) $e->getCode(),\n $e->getResponse() ? $e->getResponse()->getHeaders() : null,\n $e->getResponse() ? (string) $e->getResponse()->getBody() : null\n );\n } catch (ConnectException $e) {\n throw new ApiException(\n \"[{$e->getCode()}] {$e->getMessage()}\",\n (int) $e->getCode(),\n null,\n null\n );\n }\n\n $statusCode = $response->getStatusCode();\n\n if ($statusCode < 200 || $statusCode > 299) {\n throw new ApiException(\n sprintf(\n '[%d] Error connecting to the API (%s)',\n $statusCode,\n (string) $request->getUri()\n ),\n $statusCode,\n $response->getHeaders(),\n (string) $response->getBody()\n );\n }\n\n switch($statusCode) {\n case 200:\n if ('\\HubSpot\\Client\\Crm\\Deals\\Model\\CollectionResponseSimplePublicObjectWithAssociationsForwardPaging' === '\\SplFileObject') {\n $content = $response->getBody(); //stream goes to serializer\n } else {\n $content = (string) $response->getBody();\n }\n\n return [\n ObjectSerializer::deserialize($content, '\\HubSpot\\Client\\Crm\\Deals\\Model\\CollectionResponseSimplePublicObjectWithAssociationsForwardPaging', []),\n $response->getStatusCode(),\n $response->getHeaders()\n ];\n default:\n if ('\\HubSpot\\Client\\Crm\\Deals\\Model\\Error' === '\\SplFileObject') {\n $content = $response->getBody(); //stream goes to serializer\n } else {\n $content = (string) $response->getBody();\n }\n\n return [\n ObjectSerializer::deserialize($content, '\\HubSpot\\Client\\Crm\\Deals\\Model\\Error', []),\n $response->getStatusCode(),\n $response->getHeaders()\n ];\n }\n\n $returnType = '\\HubSpot\\Client\\Crm\\Deals\\Model\\CollectionResponseSimplePublicObjectWithAssociationsForwardPaging';\n if ($returnType === '\\SplFileObject') {\n $content = $response->getBody(); //stream goes to serializer\n } else {\n $content = (string) $response->getBody();\n }\n\n return [\n ObjectSerializer::deserialize($content, $returnType, []),\n $response->getStatusCode(),\n $response->getHeaders()\n ];\n\n } catch (ApiException $e) {\n switch ($e->getCode()) {\n case 200:\n $data = ObjectSerializer::deserialize(\n $e->getResponseBody(),\n '\\HubSpot\\Client\\Crm\\Deals\\Model\\CollectionResponseSimplePublicObjectWithAssociationsForwardPaging',\n $e->getResponseHeaders()\n );\n $e->setResponseObject($data);\n break;\n default:\n $data = ObjectSerializer::deserialize(\n $e->getResponseBody(),\n '\\HubSpot\\Client\\Crm\\Deals\\Model\\Error',\n $e->getResponseHeaders()\n );\n $e->setResponseObject($data);\n break;\n }\n throw $e;\n }\n }\n\n /**\n * Operation getPageAsync\n *\n * List\n *\n * @param int $limit The maximum number of results to display per page. (optional, default to 10)\n * @param string $after The paging cursor token of the last successfully read resource will be returned as the `paging.next.after` JSON property of a paged response containing more results. (optional)\n * @param string[] $properties A comma separated list of the properties to be returned in the response. If any of the specified properties are not present on the requested object(s), they will be ignored. (optional)\n * @param string[] $associations A comma separated list of object types to retrieve associated IDs for. If any of the specified associations do not exist, they will be ignored. (optional)\n * @param bool $archived Whether to return only results that have been archived. (optional, default to false)\n *\n * @throws \\InvalidArgumentException\n * @return \\GuzzleHttp\\Promise\\PromiseInterface\n */\n public function getPageAsync($limit = 10, $after = null, $properties = null, $associations = null, $archived = false)\n {\n return $this->getPageAsyncWithHttpInfo($limit, $after, $properties, $associations, $archived)\n ->then(\n function ($response) {\n return $response[0];\n }\n );\n }\n\n /**\n * Operation getPageAsyncWithHttpInfo\n *\n * List\n *\n * @param int $limit The maximum number of results to display per page. (optional, default to 10)\n * @param string $after The paging cursor token of the last successfully read resource will be returned as the `paging.next.after` JSON property of a paged response containing more results. (optional)\n * @param string[] $properties A comma separated list of the properties to be returned in the response. If any of the specified properties are not present on the requested object(s), they will be ignored. (optional)\n * @param string[] $associations A comma separated list of object types to retrieve associated IDs for. If any of the specified associations do not exist, they will be ignored. (optional)\n * @param bool $archived Whether to return only results that have been archived. (optional, default to false)\n *\n * @throws \\InvalidArgumentException\n * @return \\GuzzleHttp\\Promise\\PromiseInterface\n */\n public function getPageAsyncWithHttpInfo($limit = 10, $after = null, $properties = null, $associations = null, $archived = false)\n {\n $returnType = '\\HubSpot\\Client\\Crm\\Deals\\Model\\CollectionResponseSimplePublicObjectWithAssociationsForwardPaging';\n $request = $this->getPageRequest($limit, $after, $properties, $associations, $archived);\n\n return $this->client\n ->sendAsync($request, $this->createHttpClientOption())\n ->then(\n function ($response) use ($returnType) {\n if ($returnType === '\\SplFileObject') {\n $content = $response->getBody(); //stream goes to serializer\n } else {\n $content = (string) $response->getBody();\n }\n\n return [\n ObjectSerializer::deserialize($content, $returnType, []),\n $response->getStatusCode(),\n $response->getHeaders()\n ];\n },\n function ($exception) {\n $response = $exception->getResponse();\n $statusCode = $response->getStatusCode();\n throw new ApiException(\n sprintf(\n '[%d] Error connecting to the API (%s)',\n $statusCode,\n $exception->getRequest()->getUri()\n ),\n $statusCode,\n $response->getHeaders(),\n (string) $response->getBody()\n );\n }\n );\n }\n\n /**\n * Create request for operation 'getPage'\n *\n * @param int $limit The maximum number of results to display per page. (optional, default to 10)\n * @param string $after The paging cursor token of the last successfully read resource will be returned as the `paging.next.after` JSON property of a paged response containing more results. (optional)\n * @param string[] $properties A comma separated list of the properties to be returned in the response. If any of the specified properties are not present on the requested object(s), they will be ignored. (optional)\n * @param string[] $associations A comma separated list of object types to retrieve associated IDs for. If any of the specified associations do not exist, they will be ignored. (optional)\n * @param bool $archived Whether to return only results that have been archived. (optional, default to false)\n *\n * @throws \\InvalidArgumentException\n * @return \\GuzzleHttp\\Psr7\\Request\n */\n public function getPageRequest($limit = 10, $after = null, $properties = null, $associations = null, $archived = false)\n {\n\n $resourcePath = '/crm/v3/objects/deals';\n $formParams = [];\n $queryParams = [];\n $headerParams = [];\n $httpBody = '';\n $multipart = false;\n\n // query params\n if ($limit !== null) {\n if('form' === 'form' && is_array($limit)) {\n foreach($limit as $key => $value) {\n $queryParams[$key] = $value;\n }\n }\n else {\n $queryParams['limit'] = $limit;\n }\n }\n // query params\n if ($after !== null) {\n if('form' === 'form' && is_array($after)) {\n foreach($after as $key => $value) {\n $queryParams[$key] = $value;\n }\n }\n else {\n $queryParams['after'] = $after;\n }\n }\n // query params\n if ($properties !== null) {\n if('form' === 'form' && is_array($properties)) {\n foreach($properties as $key => $value) {\n $queryParams[$key] = $value;\n }\n }\n else {\n $queryParams['properties'] = $properties;\n }\n }\n // query params\n if ($associations !== null) {\n if('form' === 'form' && is_array($associations)) {\n foreach($associations as $key => $value) {\n $queryParams[$key] = $value;\n }\n }\n else {\n $queryParams['associations'] = $associations;\n }\n }\n // query params\n if ($archived !== null) {\n if('form' === 'form' && is_array($archived)) {\n foreach($archived as $key => $value) {\n $queryParams[$key] = $value;\n }\n }\n else {\n $queryParams['archived'] = $archived;\n }\n }\n\n\n\n\n if ($multipart) {\n $headers = $this->headerSelector->selectHeadersForMultipart(\n ['application/json', '*/*']\n );\n } else {\n $headers = $this->headerSelector->selectHeaders(\n ['application/json', '*/*'],\n []\n );\n }\n\n // for model (json/xml)\n if (count($formParams) > 0) {\n if ($multipart) {\n $multipartContents = [];\n foreach ($formParams as $formParamName => $formParamValue) {\n $formParamValueItems = is_array($formParamValue) ? $formParamValue : [$formParamValue];\n foreach ($formParamValueItems as $formParamValueItem) {\n $multipartContents[] = [\n 'name' => $formParamName,\n 'contents' => $formParamValueItem\n ];\n }\n }\n // for HTTP post (form)\n $httpBody = new MultipartStream($multipartContents);\n\n } elseif ($headers['Content-Type'] === 'application/json') {\n $httpBody = \\GuzzleHttp\\json_encode($formParams);\n\n } else {\n // for HTTP post (form)\n $httpBody = \\GuzzleHttp\\Psr7\\Query::build($formParams);\n }\n }\n\n // this endpoint requires API key authentication\n $apiKey = $this->config->getApiKeyWithPrefix('hapikey');\n if ($apiKey !== null) {\n $queryParams['hapikey'] = $apiKey;\n }\n // this endpoint requires OAuth (access token)\n if ($this->config->getAccessToken() !== null) {\n $headers['Authorization'] = 'Bearer ' . $this->config->getAccessToken();\n }\n\n $defaultHeaders = [];\n if ($this->config->getUserAgent()) {\n $defaultHeaders['User-Agent'] = $this->config->getUserAgent();\n }\n\n $headers = array_merge(\n $defaultHeaders,\n $headerParams,\n $headers\n );\n\n $query = \\GuzzleHttp\\Psr7\\Query::build($queryParams);\n return new Request(\n 'GET',\n $this->config->getHost() . $resourcePath . ($query ? \"?{$query}\" : ''),\n $headers,\n $httpBody\n );\n }\n\n /**\n * Operation update\n *\n * Update\n *\n * @param string $deal_id deal_id (required)\n * @param \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectInput $simple_public_object_input simple_public_object_input (required)\n * @param string $id_property The name of a property whose values are unique for this object type (optional)\n *\n * @throws \\HubSpot\\Client\\Crm\\Deals\\ApiException on non-2xx response\n * @throws \\InvalidArgumentException\n * @return \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObject|\\HubSpot\\Client\\Crm\\Deals\\Model\\Error\n */\n public function update($deal_id, $simple_public_object_input, $id_property = null)\n {\n list($response) = $this->updateWithHttpInfo($deal_id, $simple_public_object_input, $id_property);\n return $response;\n }\n\n /**\n * Operation updateWithHttpInfo\n *\n * Update\n *\n * @param string $deal_id (required)\n * @param \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectInput $simple_public_object_input (required)\n * @param string $id_property The name of a property whose values are unique for this object type (optional)\n *\n * @throws \\HubSpot\\Client\\Crm\\Deals\\ApiException on non-2xx response\n * @throws \\InvalidArgumentException\n * @return array of \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObject|\\HubSpot\\Client\\Crm\\Deals\\Model\\Error, HTTP status code, HTTP response headers (array of strings)\n */\n public function updateWithHttpInfo($deal_id, $simple_public_object_input, $id_property = null)\n {\n $request = $this->updateRequest($deal_id, $simple_public_object_input, $id_property);\n\n try {\n $options = $this->createHttpClientOption();\n try {\n $response = $this->client->send($request, $options);\n } catch (RequestException $e) {\n throw new ApiException(\n \"[{$e->getCode()}] {$e->getMessage()}\",\n (int) $e->getCode(),\n $e->getResponse() ? $e->getResponse()->getHeaders() : null,\n $e->getResponse() ? (string) $e->getResponse()->getBody() : null\n );\n } catch (ConnectException $e) {\n throw new ApiException(\n \"[{$e->getCode()}] {$e->getMessage()}\",\n (int) $e->getCode(),\n null,\n null\n );\n }\n\n $statusCode = $response->getStatusCode();\n\n if ($statusCode < 200 || $statusCode > 299) {\n throw new ApiException(\n sprintf(\n '[%d] Error connecting to the API (%s)',\n $statusCode,\n (string) $request->getUri()\n ),\n $statusCode,\n $response->getHeaders(),\n (string) $response->getBody()\n );\n }\n\n switch($statusCode) {\n case 200:\n if ('\\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObject' === '\\SplFileObject') {\n $content = $response->getBody(); //stream goes to serializer\n } else {\n $content = (string) $response->getBody();\n }\n\n return [\n ObjectSerializer::deserialize($content, '\\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObject', []),\n $response->getStatusCode(),\n $response->getHeaders()\n ];\n default:\n if ('\\HubSpot\\Client\\Crm\\Deals\\Model\\Error' === '\\SplFileObject') {\n $content = $response->getBody(); //stream goes to serializer\n } else {\n $content = (string) $response->getBody();\n }\n\n return [\n ObjectSerializer::deserialize($content, '\\HubSpot\\Client\\Crm\\Deals\\Model\\Error', []),\n $response->getStatusCode(),\n $response->getHeaders()\n ];\n }\n\n $returnType = '\\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObject';\n if ($returnType === '\\SplFileObject') {\n $content = $response->getBody(); //stream goes to serializer\n } else {\n $content = (string) $response->getBody();\n }\n\n return [\n ObjectSerializer::deserialize($content, $returnType, []),\n $response->getStatusCode(),\n $response->getHeaders()\n ];\n\n } catch (ApiException $e) {\n switch ($e->getCode()) {\n case 200:\n $data = ObjectSerializer::deserialize(\n $e->getResponseBody(),\n '\\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObject',\n $e->getResponseHeaders()\n );\n $e->setResponseObject($data);\n break;\n default:\n $data = ObjectSerializer::deserialize(\n $e->getResponseBody(),\n '\\HubSpot\\Client\\Crm\\Deals\\Model\\Error',\n $e->getResponseHeaders()\n );\n $e->setResponseObject($data);\n break;\n }\n throw $e;\n }\n }\n\n /**\n * Operation updateAsync\n *\n * Update\n *\n * @param string $deal_id (required)\n * @param \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectInput $simple_public_object_input (required)\n * @param string $id_property The name of a property whose values are unique for this object type (optional)\n *\n * @throws \\InvalidArgumentException\n * @return \\GuzzleHttp\\Promise\\PromiseInterface\n */\n public function updateAsync($deal_id, $simple_public_object_input, $id_property = null)\n {\n return $this->updateAsyncWithHttpInfo($deal_id, $simple_public_object_input, $id_property)\n ->then(\n function ($response) {\n return $response[0];\n }\n );\n }\n\n /**\n * Operation updateAsyncWithHttpInfo\n *\n * Update\n *\n * @param string $deal_id (required)\n * @param \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectInput $simple_public_object_input (required)\n * @param string $id_property The name of a property whose values are unique for this object type (optional)\n *\n * @throws \\InvalidArgumentException\n * @return \\GuzzleHttp\\Promise\\PromiseInterface\n */\n public function updateAsyncWithHttpInfo($deal_id, $simple_public_object_input, $id_property = null)\n {\n $returnType = '\\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObject';\n $request = $this->updateRequest($deal_id, $simple_public_object_input, $id_property);\n\n return $this->client\n ->sendAsync($request, $this->createHttpClientOption())\n ->then(\n function ($response) use ($returnType) {\n if ($returnType === '\\SplFileObject') {\n $content = $response->getBody(); //stream goes to serializer\n } else {\n $content = (string) $response->getBody();\n }\n\n return [\n ObjectSerializer::deserialize($content, $returnType, []),\n $response->getStatusCode(),\n $response->getHeaders()\n ];\n },\n function ($exception) {\n $response = $exception->getResponse();\n $statusCode = $response->getStatusCode();\n throw new ApiException(\n sprintf(\n '[%d] Error connecting to the API (%s)',\n $statusCode,\n $exception->getRequest()->getUri()\n ),\n $statusCode,\n $response->getHeaders(),\n (string) $response->getBody()\n );\n }\n );\n }\n\n /**\n * Create request for operation 'update'\n *\n * @param string $deal_id (required)\n * @param \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectInput $simple_public_object_input (required)\n * @param string $id_property The name of a property whose values are unique for this object type (optional)\n *\n * @throws \\InvalidArgumentException\n * @return \\GuzzleHttp\\Psr7\\Request\n */\n public function updateRequest($deal_id, $simple_public_object_input, $id_property = null)\n {\n // verify the required parameter 'deal_id' is set\n if ($deal_id === null || (is_array($deal_id) && count($deal_id) === 0)) {\n throw new \\InvalidArgumentException(\n 'Missing the required parameter $deal_id when calling update'\n );\n }\n // verify the required parameter 'simple_public_object_input' is set\n if ($simple_public_object_input === null || (is_array($simple_public_object_input) && count($simple_public_object_input) === 0)) {\n throw new \\InvalidArgumentException(\n 'Missing the required parameter $simple_public_object_input when calling update'\n );\n }\n\n $resourcePath = '/crm/v3/objects/deals/{dealId}';\n $formParams = [];\n $queryParams = [];\n $headerParams = [];\n $httpBody = '';\n $multipart = false;\n\n // query params\n if ($id_property !== null) {\n if('form' === 'form' && is_array($id_property)) {\n foreach($id_property as $key => $value) {\n $queryParams[$key] = $value;\n }\n }\n else {\n $queryParams['idProperty'] = $id_property;\n }\n }\n\n\n // path params\n if ($deal_id !== null) {\n $resourcePath = str_replace(\n '{' . 'dealId' . '}',\n ObjectSerializer::toPathValue($deal_id),\n $resourcePath\n );\n }\n\n\n if ($multipart) {\n $headers = $this->headerSelector->selectHeadersForMultipart(\n ['application/json', '*/*']\n );\n } else {\n $headers = $this->headerSelector->selectHeaders(\n ['application/json', '*/*'],\n ['application/json']\n );\n }\n\n // for model (json/xml)\n if (isset($simple_public_object_input)) {\n if ($headers['Content-Type'] === 'application/json') {\n $httpBody = \\GuzzleHttp\\json_encode(ObjectSerializer::sanitizeForSerialization($simple_public_object_input));\n } else {\n $httpBody = $simple_public_object_input;\n }\n } elseif (count($formParams) > 0) {\n if ($multipart) {\n $multipartContents = [];\n foreach ($formParams as $formParamName => $formParamValue) {\n $formParamValueItems = is_array($formParamValue) ? $formParamValue : [$formParamValue];\n foreach ($formParamValueItems as $formParamValueItem) {\n $multipartContents[] = [\n 'name' => $formParamName,\n 'contents' => $formParamValueItem\n ];\n }\n }\n // for HTTP post (form)\n $httpBody = new MultipartStream($multipartContents);\n\n } elseif ($headers['Content-Type'] === 'application/json') {\n $httpBody = \\GuzzleHttp\\json_encode($formParams);\n\n } else {\n // for HTTP post (form)\n $httpBody = \\GuzzleHttp\\Psr7\\Query::build($formParams);\n }\n }\n\n // this endpoint requires API key authentication\n $apiKey = $this->config->getApiKeyWithPrefix('hapikey');\n if ($apiKey !== null) {\n $queryParams['hapikey'] = $apiKey;\n }\n // this endpoint requires OAuth (access token)\n if ($this->config->getAccessToken() !== null) {\n $headers['Authorization'] = 'Bearer ' . $this->config->getAccessToken();\n }\n\n $defaultHeaders = [];\n if ($this->config->getUserAgent()) {\n $defaultHeaders['User-Agent'] = $this->config->getUserAgent();\n }\n\n $headers = array_merge(\n $defaultHeaders,\n $headerParams,\n $headers\n );\n\n $query = \\GuzzleHttp\\Psr7\\Query::build($queryParams);\n return new Request(\n 'PATCH',\n $this->config->getHost() . $resourcePath . ($query ? \"?{$query}\" : ''),\n $headers,\n $httpBody\n );\n }\n\n /**\n * Create http client option\n *\n * @throws \\RuntimeException on file opening failure\n * @return array of http client options\n */\n protected function createHttpClientOption()\n {\n $options = [];\n if ($this->config->getDebug()) {\n $options[RequestOptions::DEBUG] = fopen($this->config->getDebugFile(), 'a');\n if (!$options[RequestOptions::DEBUG]) {\n throw new \\RuntimeException('Failed to open the debug file: ' . $this->config->getDebugFile());\n }\n }\n\n return $options;\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"app ~/jiminny/app, folder","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".circleci, folder","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".cursor, folder","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".github","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".sonarlint, folder","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".vscode, folder","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".windsurf, folder","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"app, sources root","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Actions, folder","depth":8,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Component, folder","depth":8,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Acl, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ActionItems, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Activity, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ActivityAnalytics, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ActivitySearch, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"AiActivityType, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"AiAutomation, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"AiCallScoring, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"AskAnything, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Dtos, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Events, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"AskAnythingPromptService.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"HistoryService.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"AskJiminnyAi, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"AWS, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"BillingManagement, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Cache, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"CoachingFeedback, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Country, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"CustomerApi, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Database, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Datadog, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"DateTime, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"DealInsights, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"DealRisks","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ElasticSearch","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Eloquent","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Encoding","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Encryption","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ES","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Faker","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"FeatureFlags","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"FFMpeg","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"FileSystem","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Gecko","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Gong","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"GuzzleHttp","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"KeyPoints","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Kiosk","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"LanguageDetection","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"LiveFeed","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Locks","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Math","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"MediaPipeline","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"MeetingBot, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"MobileSettings, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Model, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Notification, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Nudge, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ParagraphBreaker, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ParticipantSpeech, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"PartitionedCookie, folder","depth":9,"on_screen":false,"role_description":"text"}]...
|
5871461784004157735
|
252663743049766078
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
Editor for custom.log
Code changed:
Hide
Sync Changes
Hide This Notification
Reader Mode
Analyzing…
<?php
/**
* BasicApi
* PHP version 7.3
*
* @category Class
* @package HubSpot\Client\Crm\Deals
* @author OpenAPI Generator team
* @link [URL_WITH_CREDENTIALS] Class
* @package HubSpot\Client\Crm\Deals
* @author OpenAPI Generator team
* @link [URL_WITH_CREDENTIALS] int $hostIndex Host index (required)
*/
public function setHostIndex($hostIndex): void
{
$this->hostIndex = $hostIndex;
}
/**
* Get the host index
*
* @return int Host index
*/
public function getHostIndex()
{
return $this->hostIndex;
}
/**
* @return Configuration
*/
public function getConfig()
{
return $this->config;
}
/**
* Operation archive
*
* Archive
*
* @param string $deal_id deal_id (required)
*
* @throws \HubSpot\Client\Crm\Deals\ApiException on non-2xx response
* @throws \InvalidArgumentException
* @return void
*/
public function archive($deal_id)
{
$this->archiveWithHttpInfo($deal_id);
}
/**
* Operation archiveWithHttpInfo
*
* Archive
*
* @param string $deal_id (required)
*
* @throws \HubSpot\Client\Crm\Deals\ApiException on non-2xx response
* @throws \InvalidArgumentException
* @return array of null, HTTP status code, HTTP response headers (array of strings)
*/
public function archiveWithHttpInfo($deal_id)
{
$request = $this->archiveRequest($deal_id);
try {
$options = $this->createHttpClientOption();
try {
$response = $this->client->send($request, $options);
} catch (RequestException $e) {
throw new ApiException(
"[{$e->getCode()}] {$e->getMessage()}",
(int) $e->getCode(),
$e->getResponse() ? $e->getResponse()->getHeaders() : null,
$e->getResponse() ? (string) $e->getResponse()->getBody() : null
);
} catch (ConnectException $e) {
throw new ApiException(
"[{$e->getCode()}] {$e->getMessage()}",
(int) $e->getCode(),
null,
null
);
}
$statusCode = $response->getStatusCode();
if ($statusCode < 200 || $statusCode > 299) {
throw new ApiException(
sprintf(
'[%d] Error connecting to the API (%s)',
$statusCode,
(string) $request->getUri()
),
$statusCode,
$response->getHeaders(),
(string) $response->getBody()
);
}
return [null, $statusCode, $response->getHeaders()];
} catch (ApiException $e) {
switch ($e->getCode()) {
default:
$data = ObjectSerializer::deserialize(
$e->getResponseBody(),
'\HubSpot\Client\Crm\Deals\Model\Error',
$e->getResponseHeaders()
);
$e->setResponseObject($data);
break;
}
throw $e;
}
}
/**
* Operation archiveAsync
*
* Archive
*
* @param string $deal_id (required)
*
* @throws \InvalidArgumentException
* @return \GuzzleHttp\Promise\PromiseInterface
*/
public function archiveAsync($deal_id)
{
return $this->archiveAsyncWithHttpInfo($deal_id)
->then(
function ($response) {
return $response[0];
}
);
}
/**
* Operation archiveAsyncWithHttpInfo
*
* Archive
*
* @param string $deal_id (required)
*
* @throws \InvalidArgumentException
* @return \GuzzleHttp\Promise\PromiseInterface
*/
public function archiveAsyncWithHttpInfo($deal_id)
{
$returnType = '';
$request = $this->archiveRequest($deal_id);
return $this->client
->sendAsync($request, $this->createHttpClientOption())
->then(
function ($response) use ($returnType) {
return [null, $response->getStatusCode(), $response->getHeaders()];
},
function ($exception) {
$response = $exception->getResponse();
$statusCode = $response->getStatusCode();
throw new ApiException(
sprintf(
'[%d] Error connecting to the API (%s)',
$statusCode,
$exception->getRequest()->getUri()
),
$statusCode,
$response->getHeaders(),
(string) $response->getBody()
);
}
);
}
/**
* Create request for operation 'archive'
*
* @param string $deal_id (required)
*
* @throws \InvalidArgumentException
* @return \GuzzleHttp\Psr7\Request
*/
public function archiveRequest($deal_id)
{
// verify the required parameter 'deal_id' is set
if ($deal_id === null || (is_array($deal_id) && count($deal_id) === 0)) {
throw new \InvalidArgumentException(
'Missing the required parameter $deal_id when calling archive'
);
}
$resourcePath = '/crm/v3/objects/deals/{dealId}';
$formParams = [];
$queryParams = [];
$headerParams = [];
$httpBody = '';
$multipart = false;
// path params
if ($deal_id !== null) {
$resourcePath = str_replace(
'{' . 'dealId' . '}',
ObjectSerializer::toPathValue($deal_id),
$resourcePath
);
}
if ($multipart) {
$headers = $this->headerSelector->selectHeadersForMultipart(
['*/*']
);
} else {
$headers = $this->headerSelector->selectHeaders(
['*/*'],
[]
);
}
// for model (json/xml)
if (count($formParams) > 0) {
if ($multipart) {
$multipartContents = [];
foreach ($formParams as $formParamName => $formParamValue) {
$formParamValueItems = is_array($formParamValue) ? $formParamValue : [$formParamValue];
foreach ($formParamValueItems as $formParamValueItem) {
$multipartContents[] = [
'name' => $formParamName,
'contents' => $formParamValueItem
];
}
}
// for HTTP post (form)
$httpBody = new MultipartStream($multipartContents);
} elseif ($headers['Content-Type'] === 'application/json') {
$httpBody = \GuzzleHttp\json_encode($formParams);
} else {
// for HTTP post (form)
$httpBody = \GuzzleHttp\Psr7\Query::build($formParams);
}
}
// this endpoint requires API key authentication
$apiKey = $this->config->getApiKeyWithPrefix('hapikey');
if ($apiKey !== null) {
$queryParams['hapikey'] = $apiKey;
}
// this endpoint requires OAuth (access token)
if ($this->config->getAccessToken() !== null) {
$headers['Authorization'] = 'Bearer ' . $this->config->getAccessToken();
}
$defaultHeaders = [];
if ($this->config->getUserAgent()) {
$defaultHeaders['User-Agent'] = $this->config->getUserAgent();
}
$headers = array_merge(
$defaultHeaders,
$headerParams,
$headers
);
$query = \GuzzleHttp\Psr7\Query::build($queryParams);
return new Request(
'DELETE',
$this->config->getHost() . $resourcePath . ($query ? "?{$query}" : ''),
$headers,
$httpBody
);
}
/**
* Operation create
*
* Create
*
* @param \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectInput $simple_public_object_input simple_public_object_input (required)
*
* @throws \HubSpot\Client\Crm\Deals\ApiException on non-2xx response
* @throws \InvalidArgumentException
* @return \HubSpot\Client\Crm\Deals\Model\SimplePublicObject|\HubSpot\Client\Crm\Deals\Model\Error
*/
public function create($simple_public_object_input)
{
list($response) = $this->createWithHttpInfo($simple_public_object_input);
return $response;
}
/**
* Operation createWithHttpInfo
*
* Create
*
* @param \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectInput $simple_public_object_input (required)
*
* @throws \HubSpot\Client\Crm\Deals\ApiException on non-2xx response
* @throws \InvalidArgumentException
* @return array of \HubSpot\Client\Crm\Deals\Model\SimplePublicObject|\HubSpot\Client\Crm\Deals\Model\Error, HTTP status code, HTTP response headers (array of strings)
*/
public function createWithHttpInfo($simple_public_object_input)
{
$request = $this->createRequest($simple_public_object_input);
try {
$options = $this->createHttpClientOption();
try {
$response = $this->client->send($request, $options);
} catch (RequestException $e) {
throw new ApiException(
"[{$e->getCode()}] {$e->getMessage()}",
(int) $e->getCode(),
$e->getResponse() ? $e->getResponse()->getHeaders() : null,
$e->getResponse() ? (string) $e->getResponse()->getBody() : null
);
} catch (ConnectException $e) {
throw new ApiException(
"[{$e->getCode()}] {$e->getMessage()}",
(int) $e->getCode(),
null,
null
);
}
$statusCode = $response->getStatusCode();
if ($statusCode < 200 || $statusCode > 299) {
throw new ApiException(
sprintf(
'[%d] Error connecting to the API (%s)',
$statusCode,
(string) $request->getUri()
),
$statusCode,
$response->getHeaders(),
(string) $response->getBody()
);
}
switch($statusCode) {
case 201:
if ('\HubSpot\Client\Crm\Deals\Model\SimplePublicObject' === '\SplFileObject') {
$content = $response->getBody(); //stream goes to serializer
} else {
$content = (string) $response->getBody();
}
return [
ObjectSerializer::deserialize($content, '\HubSpot\Client\Crm\Deals\Model\SimplePublicObject', []),
$response->getStatusCode(),
$response->getHeaders()
];
default:
if ('\HubSpot\Client\Crm\Deals\Model\Error' === '\SplFileObject') {
$content = $response->getBody(); //stream goes to serializer
} else {
$content = (string) $response->getBody();
}
return [
ObjectSerializer::deserialize($content, '\HubSpot\Client\Crm\Deals\Model\Error', []),
$response->getStatusCode(),
$response->getHeaders()
];
}
$returnType = '\HubSpot\Client\Crm\Deals\Model\SimplePublicObject';
if ($returnType === '\SplFileObject') {
$content = $response->getBody(); //stream goes to serializer
} else {
$content = (string) $response->getBody();
}
return [
ObjectSerializer::deserialize($content, $returnType, []),
$response->getStatusCode(),
$response->getHeaders()
];
} catch (ApiException $e) {
switch ($e->getCode()) {
case 201:
$data = ObjectSerializer::deserialize(
$e->getResponseBody(),
'\HubSpot\Client\Crm\Deals\Model\SimplePublicObject',
$e->getResponseHeaders()
);
$e->setResponseObject($data);
break;
default:
$data = ObjectSerializer::deserialize(
$e->getResponseBody(),
'\HubSpot\Client\Crm\Deals\Model\Error',
$e->getResponseHeaders()
);
$e->setResponseObject($data);
break;
}
throw $e;
}
}
/**
* Operation createAsync
*
* Create
*
* @param \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectInput $simple_public_object_input (required)
*
* @throws \InvalidArgumentException
* @return \GuzzleHttp\Promise\PromiseInterface
*/
public function createAsync($simple_public_object_input)
{
return $this->createAsyncWithHttpInfo($simple_public_object_input)
->then(
function ($response) {
return $response[0];
}
);
}
/**
* Operation createAsyncWithHttpInfo
*
* Create
*
* @param \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectInput $simple_public_object_input (required)
*
* @throws \InvalidArgumentException
* @return \GuzzleHttp\Promise\PromiseInterface
*/
public function createAsyncWithHttpInfo($simple_public_object_input)
{
$returnType = '\HubSpot\Client\Crm\Deals\Model\SimplePublicObject';
$request = $this->createRequest($simple_public_object_input);
return $this->client
->sendAsync($request, $this->createHttpClientOption())
->then(
function ($response) use ($returnType) {
if ($returnType === '\SplFileObject') {
$content = $response->getBody(); //stream goes to serializer
} else {
$content = (string) $response->getBody();
}
return [
ObjectSerializer::deserialize($content, $returnType, []),
$response->getStatusCode(),
$response->getHeaders()
];
},
function ($exception) {
$response = $exception->getResponse();
$statusCode = $response->getStatusCode();
throw new ApiException(
sprintf(
'[%d] Error connecting to the API (%s)',
$statusCode,
$exception->getRequest()->getUri()
),
$statusCode,
$response->getHeaders(),
(string) $response->getBody()
);
}
);
}
/**
* Create request for operation 'create'
*
* @param \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectInput $simple_public_object_input (required)
*
* @throws \InvalidArgumentException
* @return \GuzzleHttp\Psr7\Request
*/
public function createRequest($simple_public_object_input)
{
// verify the required parameter 'simple_public_object_input' is set
if ($simple_public_object_input === null || (is_array($simple_public_object_input) && count($simple_public_object_input) === 0)) {
throw new \InvalidArgumentException(
'Missing the required parameter $simple_public_object_input when calling create'
);
}
$resourcePath = '/crm/v3/objects/deals';
$formParams = [];
$queryParams = [];
$headerParams = [];
$httpBody = '';
$multipart = false;
if ($multipart) {
$headers = $this->headerSelector->selectHeadersForMultipart(
['application/json', '*/*']
);
} else {
$headers = $this->headerSelector->selectHeaders(
['application/json', '*/*'],
['application/json']
);
}
// for model (json/xml)
if (isset($simple_public_object_input)) {
if ($headers['Content-Type'] === 'application/json') {
$httpBody = \GuzzleHttp\json_encode(ObjectSerializer::sanitizeForSerialization($simple_public_object_input));
} else {
$httpBody = $simple_public_object_input;
}
} elseif (count($formParams) > 0) {
if ($multipart) {
$multipartContents = [];
foreach ($formParams as $formParamName => $formParamValue) {
$formParamValueItems = is_array($formParamValue) ? $formParamValue : [$formParamValue];
foreach ($formParamValueItems as $formParamValueItem) {
$multipartContents[] = [
'name' => $formParamName,
'contents' => $formParamValueItem
];
}
}
// for HTTP post (form)
$httpBody = new MultipartStream($multipartContents);
} elseif ($headers['Content-Type'] === 'application/json') {
$httpBody = \GuzzleHttp\json_encode($formParams);
} else {
// for HTTP post (form)
$httpBody = \GuzzleHttp\Psr7\Query::build($formParams);
}
}
// this endpoint requires API key authentication
$apiKey = $this->config->getApiKeyWithPrefix('hapikey');
if ($apiKey !== null) {
$queryParams['hapikey'] = $apiKey;
}
// this endpoint requires OAuth (access token)
if ($this->config->getAccessToken() !== null) {
$headers['Authorization'] = 'Bearer ' . $this->config->getAccessToken();
}
$defaultHeaders = [];
if ($this->config->getUserAgent()) {
$defaultHeaders['User-Agent'] = $this->config->getUserAgent();
}
$headers = array_merge(
$defaultHeaders,
$headerParams,
$headers
);
$query = \GuzzleHttp\Psr7\Query::build($queryParams);
return new Request(
'POST',
$this->config->getHost() . $resourcePath . ($query ? "?{$query}" : ''),
$headers,
$httpBody
);
}
/**
* Operation getById
*
* Read
*
* @param string $deal_id deal_id (required)
* @param string[] $properties A comma separated list of the properties to be returned in the response. If any of the specified properties are not present on the requested object(s), they will be ignored. (optional)
* @param string[] $associations A comma separated list of object types to retrieve associated IDs for. If any of the specified associations do not exist, they will be ignored. (optional)
* @param bool $archived Whether to return only results that have been archived. (optional, default to false)
* @param string $id_property The name of a property whose values are unique for this object type (optional)
*
* @throws \HubSpot\Client\Crm\Deals\ApiException on non-2xx response
* @throws \InvalidArgumentException
* @return \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations|\HubSpot\Client\Crm\Deals\Model\Error
*/
public function getById($deal_id, $properties = null, $associations = null, $archived = false, $id_property = null)
{
list($response) = $this->getByIdWithHttpInfo($deal_id, $properties, $associations, $archived, $id_property);
return $response;
}
/**
* Operation getByIdWithHttpInfo
*
* Read
*
* @param string $deal_id (required)
* @param string[] $properties A comma separated list of the properties to be returned in the response. If any of the specified properties are not present on the requested object(s), they will be ignored. (optional)
* @param string[] $associations A comma separated list of object types to retrieve associated IDs for. If any of the specified associations do not exist, they will be ignored. (optional)
* @param bool $archived Whether to return only results that have been archived. (optional, default to false)
* @param string $id_property The name of a property whose values are unique for this object type (optional)
*
* @throws \HubSpot\Client\Crm\Deals\ApiException on non-2xx response
* @throws \InvalidArgumentException
* @return array of \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations|\HubSpot\Client\Crm\Deals\Model\Error, HTTP status code, HTTP response headers (array of strings)
*/
public function getByIdWithHttpInfo($deal_id, $properties = null, $associations = null, $archived = false, $id_property = null)
{
$request = $this->getByIdRequest($deal_id, $properties, $associations, $archived, $id_property);
try {
$options = $this->createHttpClientOption();
try {
$response = $this->client->send($request, $options);
} catch (RequestException $e) {
throw new ApiException(
"[{$e->getCode()}] {$e->getMessage()}",
(int) $e->getCode(),
$e->getResponse() ? $e->getResponse()->getHeaders() : null,
$e->getResponse() ? (string) $e->getResponse()->getBody() : null
);
} catch (ConnectException $e) {
throw new ApiException(
"[{$e->getCode()}] {$e->getMessage()}",
(int) $e->getCode(),
null,
null
);
}
$statusCode = $response->getStatusCode();
if ($statusCode < 200 || $statusCode > 299) {
throw new ApiException(
sprintf(
'[%d] Error connecting to the API (%s)',
$statusCode,
(string) $request->getUri()
),
$statusCode,
$response->getHeaders(),
(string) $response->getBody()
);
}
switch($statusCode) {
case 200:
if ('\HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations' === '\SplFileObject') {
$content = $response->getBody(); //stream goes to serializer
} else {
$content = (string) $response->getBody();
}
return [
ObjectSerializer::deserialize($content, '\HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations', []),
$response->getStatusCode(),
$response->getHeaders()
];
default:
if ('\HubSpot\Client\Crm\Deals\Model\Error' === '\SplFileObject') {
$content = $response->getBody(); //stream goes to serializer
} else {
$content = (string) $response->getBody();
}
return [
ObjectSerializer::deserialize($content, '\HubSpot\Client\Crm\Deals\Model\Error', []),
$response->getStatusCode(),
$response->getHeaders()
];
}
$returnType = '\HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations';
if ($returnType === '\SplFileObject') {
$content = $response->getBody(); //stream goes to serializer
} else {
$content = (string) $response->getBody();
}
return [
ObjectSerializer::deserialize($content, $returnType, []),
$response->getStatusCode(),
$response->getHeaders()
];
} catch (ApiException $e) {
switch ($e->getCode()) {
case 200:
$data = ObjectSerializer::deserialize(
$e->getResponseBody(),
'\HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations',
$e->getResponseHeaders()
);
$e->setResponseObject($data);
break;
default:
$data = ObjectSerializer::deserialize(
$e->getResponseBody(),
'\HubSpot\Client\Crm\Deals\Model\Error',
$e->getResponseHeaders()
);
$e->setResponseObject($data);
break;
}
throw $e;
}
}
/**
* Operation getByIdAsync
*
* Read
*
* @param string $deal_id (required)
* @param string[] $properties A comma separated list of the properties to be returned in the response. If any of the specified properties are not present on the requested object(s), they will be ignored. (optional)
* @param string[] $associations A comma separated list of object types to retrieve associated IDs for. If any of the specified associations do not exist, they will be ignored. (optional)
* @param bool $archived Whether to return only results that have been archived. (optional, default to false)
* @param string $id_property The name of a property whose values are unique for this object type (optional)
*
* @throws \InvalidArgumentException
* @return \GuzzleHttp\Promise\PromiseInterface
*/
public function getByIdAsync($deal_id, $properties = null, $associations = null, $archived = false, $id_property = null)
{
return $this->getByIdAsyncWithHttpInfo($deal_id, $properties, $associations, $archived, $id_property)
->then(
function ($response) {
return $response[0];
}
);
}
/**
* Operation getByIdAsyncWithHttpInfo
*
* Read
*
* @param string $deal_id (required)
* @param string[] $properties A comma separated list of the properties to be returned in the response. If any of the specified properties are not present on the requested object(s), they will be ignored. (optional)
* @param string[] $associations A comma separated list of object types to retrieve associated IDs for. If any of the specified associations do not exist, they will be ignored. (optional)
* @param bool $archived Whether to return only results that have been archived. (optional, default to false)
* @param string $id_property The name of a property whose values are unique for this object type (optional)
*
* @throws \InvalidArgumentException
* @return \GuzzleHttp\Promise\PromiseInterface
*/
public function getByIdAsyncWithHttpInfo($deal_id, $properties = null, $associations = null, $archived = false, $id_property = null)
{
$returnType = '\HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations';
$request = $this->getByIdRequest($deal_id, $properties, $associations, $archived, $id_property);
return $this->client
->sendAsync($request, $this->createHttpClientOption())
->then(
function ($response) use ($returnType) {
if ($returnType === '\SplFileObject') {
$content = $response->getBody(); //stream goes to serializer
} else {
$content = (string) $response->getBody();
}
return [
ObjectSerializer::deserialize($content, $returnType, []),
$response->getStatusCode(),
$response->getHeaders()
];
},
function ($exception) {
$response = $exception->getResponse();
$statusCode = $response->getStatusCode();
throw new ApiException(
sprintf(
'[%d] Error connecting to the API (%s)',
$statusCode,
$exception->getRequest()->getUri()
),
$statusCode,
$response->getHeaders(),
(string) $response->getBody()
);
}
);
}
/**
* Create request for operation 'getById'
*
* @param string $deal_id (required)
* @param string[] $properties A comma separated list of the properties to be returned in the response. If any of the specified properties are not present on the requested object(s), they will be ignored. (optional)
* @param string[] $associations A comma separated list of object types to retrieve associated IDs for. If any of the specified associations do not exist, they will be ignored. (optional)
* @param bool $archived Whether to return only results that have been archived. (optional, default to false)
* @param string $id_property The name of a property whose values are unique for this object type (optional)
*
* @throws \InvalidArgumentException
* @return \GuzzleHttp\Psr7\Request
*/
public function getByIdRequest($deal_id, $properties = null, $associations = null, $archived = false, $id_property = null)
{
// verify the required parameter 'deal_id' is set
if ($deal_id === null || (is_array($deal_id) && count($deal_id) === 0)) {
throw new \InvalidArgumentException(
'Missing the required parameter $deal_id when calling getById'
);
}
$resourcePath = '/crm/v3/objects/deals/{dealId}';
$formParams = [];
$queryParams = [];
$headerParams = [];
$httpBody = '';
$multipart = false;
// query params
if ($properties !== null) {
if('form' === 'form' && is_array($properties)) {
foreach($properties as $key => $value) {
$queryParams[$key] = $value;
}
}
else {
$queryParams['properties'] = $properties;
}
}
// query params
if ($associations !== null) {
if('form' === 'form' && is_array($associations)) {
foreach($associations as $key => $value) {
$queryParams[$key] = $value;
}
}
else {
$queryParams['associations'] = $associations;
}
}
// query params
if ($archived !== null) {
if('form' === 'form' && is_array($archived)) {
foreach($archived as $key => $value) {
$queryParams[$key] = $value;
}
}
else {
$queryParams['archived'] = $archived;
}
}
// query params
if ($id_property !== null) {
if('form' === 'form' && is_array($id_property)) {
foreach($id_property as $key => $value) {
$queryParams[$key] = $value;
}
}
else {
$queryParams['idProperty'] = $id_property;
}
}
// path params
if ($deal_id !== null) {
$resourcePath = str_replace(
'{' . 'dealId' . '}',
ObjectSerializer::toPathValue($deal_id),
$resourcePath
);
}
if ($multipart) {
$headers = $this->headerSelector->selectHeadersForMultipart(
['application/json', '*/*']
);
} else {
$headers = $this->headerSelector->selectHeaders(
['application/json', '*/*'],
[]
);
}
// for model (json/xml)
if (count($formParams) > 0) {
if ($multipart) {
$multipartContents = [];
foreach ($formParams as $formParamName => $formParamValue) {
$formParamValueItems = is_array($formParamValue) ? $formParamValue : [$formParamValue];
foreach ($formParamValueItems as $formParamValueItem) {
$multipartContents[] = [
'name' => $formParamName,
'contents' => $formParamValueItem
];
}
}
// for HTTP post (form)
$httpBody = new MultipartStream($multipartContents);
} elseif ($headers['Content-Type'] === 'application/json') {
$httpBody = \GuzzleHttp\json_encode($formParams);
} else {
// for HTTP post (form)
$httpBody = \GuzzleHttp\Psr7\Query::build($formParams);
}
}
// this endpoint requires API key authentication
$apiKey = $this->config->getApiKeyWithPrefix('hapikey');
if ($apiKey !== null) {
$queryParams['hapikey'] = $apiKey;
}
// this endpoint requires OAuth (access token)
if ($this->config->getAccessToken() !== null) {
$headers['Authorization'] = 'Bearer ' . $this->config->getAccessToken();
}
$defaultHeaders = [];
if ($this->config->getUserAgent()) {
$defaultHeaders['User-Agent'] = $this->config->getUserAgent();
}
$headers = array_merge(
$defaultHeaders,
$headerParams,
$headers
);
$query = \GuzzleHttp\Psr7\Query::build($queryParams);
return new Request(
'GET',
$this->config->getHost() . $resourcePath . ($query ? "?{$query}" : ''),
$headers,
$httpBody
);
}
/**
* Operation getPage
*
* List
*
* @param int $limit The maximum number of results to display per page. (optional, default to 10)
* @param string $after The paging cursor token of the last successfully read resource will be returned as the `paging.next.after` JSON property of a paged response containing more results. (optional)
* @param string[] $properties A comma separated list of the properties to be returned in the response. If any of the specified properties are not present on the requested object(s), they will be ignored. (optional)
* @param string[] $associations A comma separated list of object types to retrieve associated IDs for. If any of the specified associations do not exist, they will be ignored. (optional)
* @param bool $archived Whether to return only results that have been archived. (optional, default to false)
*
* @throws \HubSpot\Client\Crm\Deals\ApiException on non-2xx response
* @throws \InvalidArgumentException
* @return \HubSpot\Client\Crm\Deals\Model\CollectionResponseSimplePublicObjectWithAssociationsForwardPaging|\HubSpot\Client\Crm\Deals\Model\Error
*/
public function getPage($limit = 10, $after = null, $properties = null, $associations = null, $archived = false)
{
list($response) = $this->getPageWithHttpInfo($limit, $after, $properties, $associations, $archived);
return $response;
}
/**
* Operation getPageWithHttpInfo
*
* List
*
* @param int $limit The maximum number of results to display per page. (optional, default to 10)
* @param string $after The paging cursor token of the last successfully read resource will be returned as the `paging.next.after` JSON property of a paged response containing more results. (optional)
* @param string[] $properties A comma separated list of the properties to be returned in the response. If any of the specified properties are not present on the requested object(s), they will be ignored. (optional)
* @param string[] $associations A comma separated list of object types to retrieve associated IDs for. If any of the specified associations do not exist, they will be ignored. (optional)
* @param bool $archived Whether to return only results that have been archived. (optional, default to false)
*
* @throws \HubSpot\Client\Crm\Deals\ApiException on non-2xx response
* @throws \InvalidArgumentException
* @return array of \HubSpot\Client\Crm\Deals\Model\CollectionResponseSimplePublicObjectWithAssociationsForwardPaging|\HubSpot\Client\Crm\Deals\Model\Error, HTTP status code, HTTP response headers (array of strings)
*/
public function getPageWithHttpInfo($limit = 10, $after = null, $properties = null, $associations = null, $archived = false)
{
$request = $this->getPageRequest($limit, $after, $properties, $associations, $archived);
try {
$options = $this->createHttpClientOption();
try {
$response = $this->client->send($request, $options);
} catch (RequestException $e) {
throw new ApiException(
"[{$e->getCode()}] {$e->getMessage()}",
(int) $e->getCode(),
$e->getResponse() ? $e->getResponse()->getHeaders() : null,
$e->getResponse() ? (string) $e->getResponse()->getBody() : null
);
} catch (ConnectException $e) {
throw new ApiException(
"[{$e->getCode()}] {$e->getMessage()}",
(int) $e->getCode(),
null,
null
);
}
$statusCode = $response->getStatusCode();
if ($statusCode < 200 || $statusCode > 299) {
throw new ApiException(
sprintf(
'[%d] Error connecting to the API (%s)',
$statusCode,
(string) $request->getUri()
),
$statusCode,
$response->getHeaders(),
(string) $response->getBody()
);
}
switch($statusCode) {
case 200:
if ('\HubSpot\Client\Crm\Deals\Model\CollectionResponseSimplePublicObjectWithAssociationsForwardPaging' === '\SplFileObject') {
$content = $response->getBody(); //stream goes to serializer
} else {
$content = (string) $response->getBody();
}
return [
ObjectSerializer::deserialize($content, '\HubSpot\Client\Crm\Deals\Model\CollectionResponseSimplePublicObjectWithAssociationsForwardPaging', []),
$response->getStatusCode(),
$response->getHeaders()
];
default:
if ('\HubSpot\Client\Crm\Deals\Model\Error' === '\SplFileObject') {
$content = $response->getBody(); //stream goes to serializer
} else {
$content = (string) $response->getBody();
}
return [
ObjectSerializer::deserialize($content, '\HubSpot\Client\Crm\Deals\Model\Error', []),
$response->getStatusCode(),
$response->getHeaders()
];
}
$returnType = '\HubSpot\Client\Crm\Deals\Model\CollectionResponseSimplePublicObjectWithAssociationsForwardPaging';
if ($returnType === '\SplFileObject') {
$content = $response->getBody(); //stream goes to serializer
} else {
$content = (string) $response->getBody();
}
return [
ObjectSerializer::deserialize($content, $returnType, []),
$response->getStatusCode(),
$response->getHeaders()
];
} catch (ApiException $e) {
switch ($e->getCode()) {
case 200:
$data = ObjectSerializer::deserialize(
$e->getResponseBody(),
'\HubSpot\Client\Crm\Deals\Model\CollectionResponseSimplePublicObjectWithAssociationsForwardPaging',
$e->getResponseHeaders()
);
$e->setResponseObject($data);
break;
default:
$data = ObjectSerializer::deserialize(
$e->getResponseBody(),
'\HubSpot\Client\Crm\Deals\Model\Error',
$e->getResponseHeaders()
);
$e->setResponseObject($data);
break;
}
throw $e;
}
}
/**
* Operation getPageAsync
*
* List
*
* @param int $limit The maximum number of results to display per page. (optional, default to 10)
* @param string $after The paging cursor token of the last successfully read resource will be returned as the `paging.next.after` JSON property of a paged response containing more results. (optional)
* @param string[] $properties A comma separated list of the properties to be returned in the response. If any of the specified properties are not present on the requested object(s), they will be ignored. (optional)
* @param string[] $associations A comma separated list of object types to retrieve associated IDs for. If any of the specified associations do not exist, they will be ignored. (optional)
* @param bool $archived Whether to return only results that have been archived. (optional, default to false)
*
* @throws \InvalidArgumentException
* @return \GuzzleHttp\Promise\PromiseInterface
*/
public function getPageAsync($limit = 10, $after = null, $properties = null, $associations = null, $archived = false)
{
return $this->getPageAsyncWithHttpInfo($limit, $after, $properties, $associations, $archived)
->then(
function ($response) {
return $response[0];
}
);
}
/**
* Operation getPageAsyncWithHttpInfo
*
* List
*
* @param int $limit The maximum number of results to display per page. (optional, default to 10)
* @param string $after The paging cursor token of the last successfully read resource will be returned as the `paging.next.after` JSON property of a paged response containing more results. (optional)
* @param string[] $properties A comma separated list of the properties to be returned in the response. If any of the specified properties are not present on the requested object(s), they will be ignored. (optional)
* @param string[] $associations A comma separated list of object types to retrieve associated IDs for. If any of the specified associations do not exist, they will be ignored. (optional)
* @param bool $archived Whether to return only results that have been archived. (optional, default to false)
*
* @throws \InvalidArgumentException
* @return \GuzzleHttp\Promise\PromiseInterface
*/
public function getPageAsyncWithHttpInfo($limit = 10, $after = null, $properties = null, $associations = null, $archived = false)
{
$returnType = '\HubSpot\Client\Crm\Deals\Model\CollectionResponseSimplePublicObjectWithAssociationsForwardPaging';
$request = $this->getPageRequest($limit, $after, $properties, $associations, $archived);
return $this->client
->sendAsync($request, $this->createHttpClientOption())
->then(
function ($response) use ($returnType) {
if ($returnType === '\SplFileObject') {
$content = $response->getBody(); //stream goes to serializer
} else {
$content = (string) $response->getBody();
}
return [
ObjectSerializer::deserialize($content, $returnType, []),
$response->getStatusCode(),
$response->getHeaders()
];
},
function ($exception) {
$response = $exception->getResponse();
$statusCode = $response->getStatusCode();
throw new ApiException(
sprintf(
'[%d] Error connecting to the API (%s)',
$statusCode,
$exception->getRequest()->getUri()
),
$statusCode,
$response->getHeaders(),
(string) $response->getBody()
);
}
);
}
/**
* Create request for operation 'getPage'
*
* @param int $limit The maximum number of results to display per page. (optional, default to 10)
* @param string $after The paging cursor token of the last successfully read resource will be returned as the `paging.next.after` JSON property of a paged response containing more results. (optional)
* @param string[] $properties A comma separated list of the properties to be returned in the response. If any of the specified properties are not present on the requested object(s), they will be ignored. (optional)
* @param string[] $associations A comma separated list of object types to retrieve associated IDs for. If any of the specified associations do not exist, they will be ignored. (optional)
* @param bool $archived Whether to return only results that have been archived. (optional, default to false)
*
* @throws \InvalidArgumentException
* @return \GuzzleHttp\Psr7\Request
*/
public function getPageRequest($limit = 10, $after = null, $properties = null, $associations = null, $archived = false)
{
$res...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
2886
|
115
|
6
|
2026-05-07T11:46:56.853243+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778154416853_m1.jpg...
|
PhpStorm
|
faVsco.js – ~/jiminny/app/vendor/hubspot/api-clien faVsco.js – ~/jiminny/app/vendor/hubspot/api-client/codegen/Crm/Deals/Api/BasicApi.php...
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
Editor for custom.log
Code changed:
Hide
Sync Changes
Hide This Notification
Reader Mode
Analyzing…
<?php
/**
* BasicApi
* PHP version 7.3
*
* @category Class
* @package HubSpot\Client\Crm\Deals
* @author OpenAPI Generator team
* @link [URL_WITH_CREDENTIALS] Class
* @package HubSpot\Client\Crm\Deals
* @author OpenAPI Generator team
* @link [URL_WITH_CREDENTIALS] int $hostIndex Host index (required)
*/
public function setHostIndex($hostIndex): void
{
$this->hostIndex = $hostIndex;
}
/**
* Get the host index
*
* @return int Host index
*/
public function getHostIndex()
{
return $this->hostIndex;
}
/**
* @return Configuration
*/
public function getConfig()
{
return $this->config;
}
/**
* Operation archive
*
* Archive
*
* @param string $deal_id deal_id (required)
*
* @throws \HubSpot\Client\Crm\Deals\ApiException on non-2xx response
* @throws \InvalidArgumentException
* @return void
*/
public function archive($deal_id)
{
$this->archiveWithHttpInfo($deal_id);
}
/**
* Operation archiveWithHttpInfo
*
* Archive
*
* @param string $deal_id (required)
*
* @throws \HubSpot\Client\Crm\Deals\ApiException on non-2xx response
* @throws \InvalidArgumentException
* @return array of null, HTTP status code, HTTP response headers (array of strings)
*/
public function archiveWithHttpInfo($deal_id)
{
$request = $this->archiveRequest($deal_id);
try {
$options = $this->createHttpClientOption();
try {
$response = $this->client->send($request, $options);
} catch (RequestException $e) {
throw new ApiException(
"[{$e->getCode()}] {$e->getMessage()}",
(int) $e->getCode(),
$e->getResponse() ? $e->getResponse()->getHeaders() : null,
$e->getResponse() ? (string) $e->getResponse()->getBody() : null
);
} catch (ConnectException $e) {
throw new ApiException(
"[{$e->getCode()}] {$e->getMessage()}",
(int) $e->getCode(),
null,
null
);
}
$statusCode = $response->getStatusCode();
if ($statusCode < 200 || $statusCode > 299) {
throw new ApiException(
sprintf(
'[%d] Error connecting to the API (%s)',
$statusCode,
(string) $request->getUri()
),
$statusCode,
$response->getHeaders(),
(string) $response->getBody()
);
}
return [null, $statusCode, $response->getHeaders()];
} catch (ApiException $e) {
switch ($e->getCode()) {
default:
$data = ObjectSerializer::deserialize(
$e->getResponseBody(),
'\HubSpot\Client\Crm\Deals\Model\Error',
$e->getResponseHeaders()
);
$e->setResponseObject($data);
break;
}
throw $e;
}
}
/**
* Operation archiveAsync
*
* Archive
*
* @param string $deal_id (required)
*
* @throws \InvalidArgumentException
* @return \GuzzleHttp\Promise\PromiseInterface
*/
public function archiveAsync($deal_id)
{
return $this->archiveAsyncWithHttpInfo($deal_id)
->then(
function ($response) {
return $response[0];
}
);
}
/**
* Operation archiveAsyncWithHttpInfo
*
* Archive
*
* @param string $deal_id (required)
*
* @throws \InvalidArgumentException
* @return \GuzzleHttp\Promise\PromiseInterface
*/
public function archiveAsyncWithHttpInfo($deal_id)
{
$returnType = '';
$request = $this->archiveRequest($deal_id);
return $this->client
->sendAsync($request, $this->createHttpClientOption())
->then(
function ($response) use ($returnType) {
return [null, $response->getStatusCode(), $response->getHeaders()];
},
function ($exception) {
$response = $exception->getResponse();
$statusCode = $response->getStatusCode();
throw new ApiException(
sprintf(
'[%d] Error connecting to the API (%s)',
$statusCode,
$exception->getRequest()->getUri()
),
$statusCode,
$response->getHeaders(),
(string) $response->getBody()
);
}
);
}
/**
* Create request for operation 'archive'
*
* @param string $deal_id (required)
*
* @throws \InvalidArgumentException
* @return \GuzzleHttp\Psr7\Request
*/
public function archiveRequest($deal_id)
{
// verify the required parameter 'deal_id' is set
if ($deal_id === null || (is_array($deal_id) && count($deal_id) === 0)) {
throw new \InvalidArgumentException(
'Missing the required parameter $deal_id when calling archive'
);
}
$resourcePath = '/crm/v3/objects/deals/{dealId}';
$formParams = [];
$queryParams = [];
$headerParams = [];
$httpBody = '';
$multipart = false;
// path params
if ($deal_id !== null) {
$resourcePath = str_replace(
'{' . 'dealId' . '}',
ObjectSerializer::toPathValue($deal_id),
$resourcePath
);
}
if ($multipart) {
$headers = $this->headerSelector->selectHeadersForMultipart(
['*/*']
);
} else {
$headers = $this->headerSelector->selectHeaders(
['*/*'],
[]
);
}
// for model (json/xml)
if (count($formParams) > 0) {
if ($multipart) {
$multipartContents = [];
foreach ($formParams as $formParamName => $formParamValue) {
$formParamValueItems = is_array($formParamValue) ? $formParamValue : [$formParamValue];
foreach ($formParamValueItems as $formParamValueItem) {
$multipartContents[] = [
'name' => $formParamName,
'contents' => $formParamValueItem
];
}
}
// for HTTP post (form)
$httpBody = new MultipartStream($multipartContents);
} elseif ($headers['Content-Type'] === 'application/json') {
$httpBody = \GuzzleHttp\json_encode($formParams);
} else {
// for HTTP post (form)
$httpBody = \GuzzleHttp\Psr7\Query::build($formParams);
}
}
// this endpoint requires API key authentication
$apiKey = $this->config->getApiKeyWithPrefix('hapikey');
if ($apiKey !== null) {
$queryParams['hapikey'] = $apiKey;
}
// this endpoint requires OAuth (access token)
if ($this->config->getAccessToken() !== null) {
$headers['Authorization'] = 'Bearer ' . $this->config->getAccessToken();
}
$defaultHeaders = [];
if ($this->config->getUserAgent()) {
$defaultHeaders['User-Agent'] = $this->config->getUserAgent();
}
$headers = array_merge(
$defaultHeaders,
$headerParams,
$headers
);
$query = \GuzzleHttp\Psr7\Query::build($queryParams);
return new Request(
'DELETE',
$this->config->getHost() . $resourcePath . ($query ? "?{$query}" : ''),
$headers,
$httpBody
);
}
/**
* Operation create
*
* Create
*
* @param \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectInput $simple_public_object_input simple_public_object_input (required)
*
* @throws \HubSpot\Client\Crm\Deals\ApiException on non-2xx response
* @throws \InvalidArgumentException
* @return \HubSpot\Client\Crm\Deals\Model\SimplePublicObject|\HubSpot\Client\Crm\Deals\Model\Error
*/
public function create($simple_public_object_input)
{
list($response) = $this->createWithHttpInfo($simple_public_object_input);
return $response;
}
/**
* Operation createWithHttpInfo
*
* Create
*
* @param \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectInput $simple_public_object_input (required)
*
* @throws \HubSpot\Client\Crm\Deals\ApiException on non-2xx response
* @throws \InvalidArgumentException
* @return array of \HubSpot\Client\Crm\Deals\Model\SimplePublicObject|\HubSpot\Client\Crm\Deals\Model\Error, HTTP status code, HTTP response headers (array of strings)
*/
public function createWithHttpInfo($simple_public_object_input)
{
$request = $this->createRequest($simple_public_object_input);
try {
$options = $this->createHttpClientOption();
try {
$response = $this->client->send($request, $options);
} catch (RequestException $e) {
throw new ApiException(
"[{$e->getCode()}] {$e->getMessage()}",
(int) $e->getCode(),
$e->getResponse() ? $e->getResponse()->getHeaders() : null,
$e->getResponse() ? (string) $e->getResponse()->getBody() : null
);
} catch (ConnectException $e) {
throw new ApiException(
"[{$e->getCode()}] {$e->getMessage()}",
(int) $e->getCode(),
null,
null
);
}
$statusCode = $response->getStatusCode();
if ($statusCode < 200 || $statusCode > 299) {
throw new ApiException(
sprintf(
'[%d] Error connecting to the API (%s)',
$statusCode,
(string) $request->getUri()
),
$statusCode,
$response->getHeaders(),
(string) $response->getBody()
);
}
switch($statusCode) {
case 201:
if ('\HubSpot\Client\Crm\Deals\Model\SimplePublicObject' === '\SplFileObject') {
$content = $response->getBody(); //stream goes to serializer
} else {
$content = (string) $response->getBody();
}
return [
ObjectSerializer::deserialize($content, '\HubSpot\Client\Crm\Deals\Model\SimplePublicObject', []),
$response->getStatusCode(),
$response->getHeaders()
];
default:
if ('\HubSpot\Client\Crm\Deals\Model\Error' === '\SplFileObject') {
$content = $response->getBody(); //stream goes to serializer
} else {
$content = (string) $response->getBody();
}
return [
ObjectSerializer::deserialize($content, '\HubSpot\Client\Crm\Deals\Model\Error', []),
$response->getStatusCode(),
$response->getHeaders()
];
}
$returnType = '\HubSpot\Client\Crm\Deals\Model\SimplePublicObject';
if ($returnType === '\SplFileObject') {
$content = $response->getBody(); //stream goes to serializer
} else {
$content = (string) $response->getBody();
}
return [
ObjectSerializer::deserialize($content, $returnType, []),
$response->getStatusCode(),
$response->getHeaders()
];
} catch (ApiException $e) {
switch ($e->getCode()) {
case 201:
$data = ObjectSerializer::deserialize(
$e->getResponseBody(),
'\HubSpot\Client\Crm\Deals\Model\SimplePublicObject',
$e->getResponseHeaders()
);
$e->setResponseObject($data);
break;
default:
$data = ObjectSerializer::deserialize(
$e->getResponseBody(),
'\HubSpot\Client\Crm\Deals\Model\Error',
$e->getResponseHeaders()
);
$e->setResponseObject($data);
break;
}
throw $e;
}
}
/**
* Operation createAsync
*
* Create
*
* @param \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectInput $simple_public_object_input (required)
*
* @throws \InvalidArgumentException
* @return \GuzzleHttp\Promise\PromiseInterface
*/
public function createAsync($simple_public_object_input)
{
return $this->createAsyncWithHttpInfo($simple_public_object_input)
->then(
function ($response) {
return $response[0];
}
);
}
/**
* Operation createAsyncWithHttpInfo
*
* Create
*
* @param \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectInput $simple_public_object_input (required)
*
* @throws \InvalidArgumentException
* @return \GuzzleHttp\Promise\PromiseInterface
*/
public function createAsyncWithHttpInfo($simple_public_object_input)
{
$returnType = '\HubSpot\Client\Crm\Deals\Model\SimplePublicObject';
$request = $this->createRequest($simple_public_object_input);
return $this->client
->sendAsync($request, $this->createHttpClientOption())
->then(
function ($response) use ($returnType) {
if ($returnType === '\SplFileObject') {
$content = $response->getBody(); //stream goes to serializer
} else {
$content = (string) $response->getBody();
}
return [
ObjectSerializer::deserialize($content, $returnType, []),
$response->getStatusCode(),
$response->getHeaders()
];
},
function ($exception) {
$response = $exception->getResponse();
$statusCode = $response->getStatusCode();
throw new ApiException(
sprintf(
'[%d] Error connecting to the API (%s)',
$statusCode,
$exception->getRequest()->getUri()
),
$statusCode,
$response->getHeaders(),
(string) $response->getBody()
);
}
);
}
/**
* Create request for operation 'create'
*
* @param \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectInput $simple_public_object_input (required)
*
* @throws \InvalidArgumentException
* @return \GuzzleHttp\Psr7\Request
*/
public function createRequest($simple_public_object_input)
{
// verify the required parameter 'simple_public_object_input' is set
if ($simple_public_object_input === null || (is_array($simple_public_object_input) && count($simple_public_object_input) === 0)) {
throw new \InvalidArgumentException(
'Missing the required parameter $simple_public_object_input when calling create'
);
}
$resourcePath = '/crm/v3/objects/deals';
$formParams = [];
$queryParams = [];
$headerParams = [];
$httpBody = '';
$multipart = false;
if ($multipart) {
$headers = $this->headerSelector->selectHeadersForMultipart(
['application/json', '*/*']
);
} else {
$headers = $this->headerSelector->selectHeaders(
['application/json', '*/*'],
['application/json']
);
}
// for model (json/xml)
if (isset($simple_public_object_input)) {
if ($headers['Content-Type'] === 'application/json') {
$httpBody = \GuzzleHttp\json_encode(ObjectSerializer::sanitizeForSerialization($simple_public_object_input));
} else {
$httpBody = $simple_public_object_input;
}
} elseif (count($formParams) > 0) {
if ($multipart) {
$multipartContents = [];
foreach ($formParams as $formParamName => $formParamValue) {
$formParamValueItems = is_array($formParamValue) ? $formParamValue : [$formParamValue];
foreach ($formParamValueItems as $formParamValueItem) {
$multipartContents[] = [
'name' => $formParamName,
'contents' => $formParamValueItem
];
}
}
// for HTTP post (form)
$httpBody = new MultipartStream($multipartContents);
} elseif ($headers['Content-Type'] === 'application/json') {
$httpBody = \GuzzleHttp\json_encode($formParams);
} else {
// for HTTP post (form)
$httpBody = \GuzzleHttp\Psr7\Query::build($formParams);
}
}
// this endpoint requires API key authentication
$apiKey = $this->config->getApiKeyWithPrefix('hapikey');
if ($apiKey !== null) {
$queryParams['hapikey'] = $apiKey;
}
// this endpoint requires OAuth (access token)
if ($this->config->getAccessToken() !== null) {
$headers['Authorization'] = 'Bearer ' . $this->config->getAccessToken();
}
$defaultHeaders = [];
if ($this->config->getUserAgent()) {
$defaultHeaders['User-Agent'] = $this->config->getUserAgent();
}
$headers = array_merge(
$defaultHeaders,
$headerParams,
$headers
);
$query = \GuzzleHttp\Psr7\Query::build($queryParams);
return new Request(
'POST',
$this->config->getHost() . $resourcePath . ($query ? "?{$query}" : ''),
$headers,
$httpBody
);
}
/**
* Operation getById
*
* Read
*
* @param string $deal_id deal_id (required)
* @param string[] $properties A comma separated list of the properties to be returned in the response. If any of the specified properties are not present on the requested object(s), they will be ignored. (optional)
* @param string[] $associations A comma separated list of object types to retrieve associated IDs for. If any of the specified associations do not exist, they will be ignored. (optional)
* @param bool $archived Whether to return only results that have been archived. (optional, default to false)
* @param string $id_property The name of a property whose values are unique for this object type (optional)
*
* @throws \HubSpot\Client\Crm\Deals\ApiException on non-2xx response
* @throws \InvalidArgumentException
* @return \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations|\HubSpot\Client\Crm\Deals\Model\Error
*/
public function getById($deal_id, $properties = null, $associations = null, $archived = false, $id_property = null)
{
list($response) = $this->getByIdWithHttpInfo($deal_id, $properties, $associations, $archived, $id_property);
return $response;
}
/**
* Operation getByIdWithHttpInfo
*
* Read
*
* @param string $deal_id (required)
* @param string[] $properties A comma separated list of the properties to be returned in the response. If any of the specified properties are not present on the requested object(s), they will be ignored. (optional)
* @param string[] $associations A comma separated list of object types to retrieve associated IDs for. If any of the specified associations do not exist, they will be ignored. (optional)
* @param bool $archived Whether to return only results that have been archived. (optional, default to false)
* @param string $id_property The name of a property whose values are unique for this object type (optional)
*
* @throws \HubSpot\Client\Crm\Deals\ApiException on non-2xx response
* @throws \InvalidArgumentException
* @return array of \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations|\HubSpot\Client\Crm\Deals\Model\Error, HTTP status code, HTTP response headers (array of strings)
*/
public function getByIdWithHttpInfo($deal_id, $properties = null, $associations = null, $archived = false, $id_property = null)
{
$request = $this->getByIdRequest($deal_id, $properties, $associations, $archived, $id_property);
try {
$options = $this->createHttpClientOption();
try {
$response = $this->client->send($request, $options);
} catch (RequestException $e) {
throw new ApiException(
"[{$e->getCode()}] {$e->getMessage()}",
(int) $e->getCode(),
$e->getResponse() ? $e->getResponse()->getHeaders() : null,
$e->getResponse() ? (string) $e->getResponse()->getBody() : null
);
} catch (ConnectException $e) {
throw new ApiException(
"[{$e->getCode()}] {$e->getMessage()}",
(int) $e->getCode(),
null,
null
);
}
$statusCode = $response->getStatusCode();
if ($statusCode < 200 || $statusCode > 299) {
throw new ApiException(
sprintf(
'[%d] Error connecting to the API (%s)',
$statusCode,
(string) $request->getUri()
),
$statusCode,
$response->getHeaders(),
(string) $response->getBody()
);
}
switch($statusCode) {
case 200:
if ('\HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations' === '\SplFileObject') {
$content = $response->getBody(); //stream goes to serializer
} else {
$content = (string) $response->getBody();
}
return [
ObjectSerializer::deserialize($content, '\HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations', []),
$response->getStatusCode(),
$response->getHeaders()
];
default:
if ('\HubSpot\Client\Crm\Deals\Model\Error' === '\SplFileObject') {
$content = $response->getBody(); //stream goes to serializer
} else {
$content = (string) $response->getBody();
}
return [
ObjectSerializer::deserialize($content, '\HubSpot\Client\Crm\Deals\Model\Error', []),
$response->getStatusCode(),
$response->getHeaders()
];
}
$returnType = '\HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations';
if ($returnType === '\SplFileObject') {
$content = $response->getBody(); //stream goes to serializer
} else {
$content = (string) $response->getBody();
}
return [
ObjectSerializer::deserialize($content, $returnType, []),
$response->getStatusCode(),
$response->getHeaders()
];
} catch (ApiException $e) {
switch ($e->getCode()) {
case 200:
$data = ObjectSerializer::deserialize(
$e->getResponseBody(),
'\HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations',
$e->getResponseHeaders()
);
$e->setResponseObject($data);
break;
default:
$data = ObjectSerializer::deserialize(
$e->getResponseBody(),
'\HubSpot\Client\Crm\Deals\Model\Error',
$e->getResponseHeaders()
);
$e->setResponseObject($data);
break;
}
throw $e;
}
}
/**
* Operation getByIdAsync
*
* Read
*
* @param string $deal_id (required)
* @param string[] $properties A comma separated list of the properties to be returned in the response. If any of the specified properties are not present on the requested object(s), they will be ignored. (optional)
* @param string[] $associations A comma separated list of object types to retrieve associated IDs for. If any of the specified associations do not exist, they will be ignored. (optional)
* @param bool $archived Whether to return only results that have been archived. (optional, default to false)
* @param string $id_property The name of a property whose values are unique for this object type (optional)
*
* @throws \InvalidArgumentException
* @return \GuzzleHttp\Promise\PromiseInterface
*/
public function getByIdAsync($deal_id, $properties = null, $associations = null, $archived = false, $id_property = null)
{
return $this->getByIdAsyncWithHttpInfo($deal_id, $properties, $associations, $archived, $id_property)
->then(
function ($response) {
return $response[0];
}
);
}
/**
* Operation getByIdAsyncWithHttpInfo
*
* Read
*
* @param string $deal_id (required)
* @param string[] $properties A comma separated list of the properties to be returned in the response. If any of the specified properties are not present on the requested object(s), they will be ignored. (optional)
* @param string[] $associations A comma separated list of object types to retrieve associated IDs for. If any of the specified associations do not exist, they will be ignored. (optional)
* @param bool $archived Whether to return only results that have been archived. (optional, default to false)
* @param string $id_property The name of a property whose values are unique for this object type (optional)
*
* @throws \InvalidArgumentException
* @return \GuzzleHttp\Promise\PromiseInterface
*/
public function getByIdAsyncWithHttpInfo($deal_id, $properties = null, $associations = null, $archived = false, $id_property = null)
{
$returnType = '\HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations';
$request = $this->getByIdRequest($deal_id, $properties, $associations, $archived, $id_property);
return $this->client
->sendAsync($request, $this->createHttpClientOption())
->then(
function ($response) use ($returnType) {
if ($returnType === '\SplFileObject') {
$content = $response->getBody(); //stream goes to serializer
} else {
$content = (string) $response->getBody();
}
return [
ObjectSerializer::deserialize($content, $returnType, []),
$response->getStatusCode(),
$response->getHeaders()
];
},
function ($exception) {
$response = $exception->getResponse();
$statusCode = $response->getStatusCode();
throw new ApiException(
sprintf(
'[%d] Error connecting to the API (%s)',
$statusCode,
$exception->getRequest()->getUri()
),
$statusCode,
$response->getHeaders(),
(string) $response->getBody()
);
}
);
}
/**
* Create request for operation 'getById'
*
* @param string $deal_id (required)
* @param string[] $properties A comma separated list of the properties to be returned in the response. If any of the specified properties are not present on the requested object(s), they will be ignored. (optional)
* @param string[] $associations A comma separated list of object types to retrieve associated IDs for. If any of the specified associations do not exist, they will be ignored. (optional)
* @param bool $archived Whether to return only results that have been archived. (optional, default to false)
* @param string $id_property The name of a property whose values are unique for this object type (optional)
*
* @throws \InvalidArgumentException
* @return \GuzzleHttp\Psr7\Request
*/
public function getByIdRequest($deal_id, $properties = null, $associations = null, $archived = false, $id_property = null)
{
// verify the required parameter 'deal_id' is set
if ($deal_id === null || (is_array($deal_id) && count($deal_id) === 0)) {
throw new \InvalidArgumentException(
'Missing the required parameter $deal_id when calling getById'
);
}
$resourcePath = '/crm/v3/objects/deals/{dealId}';
$formParams = [];
$queryParams = [];
$headerParams = [];
$httpBody = '';
$multipart = false;
// query params
if ($properties !== null) {
if('form' === 'form' && is_array($properties)) {
foreach($properties as $key => $value) {
$queryParams[$key] = $value;
}
}
else {
$queryParams['properties'] = $properties;
}
}
// query params
if ($associations !== null) {
if('form' === 'form' && is_array($associations)) {
foreach($associations as $key => $value) {
$queryParams[$key] = $value;
}
}
else {
$queryParams['associations'] = $associations;
}
}
// query params
if ($archived !== null) {
if('form' === 'form' && is_array($archived)) {
foreach($archived as $key => $value) {
$queryParams[$key] = $value;
}
}
else {
$queryParams['archived'] = $archived;
}
}
// query params
if ($id_property !== null) {
if('form' === 'form' && is_array($id_property)) {
foreach($id_property as $key => $value) {
$queryParams[$key] = $value;
}
}
else {
$queryParams['idProperty'] = $id_property;
}
}
// path params
if ($deal_id !== null) {
$resourcePath = str_replace(
'{' . 'dealId' . '}',
ObjectSerializer::toPathValue($deal_id),
$resourcePath
);
}
if ($multipart) {
$headers = $this->headerSelector->selectHeadersForMultipart(
['application/json', '*/*']
);
} else {
$headers = $this->headerSelector->selectHeaders(
['application/json', '*/*'],
[]
);
}
// for model (json/xml)
if (count($formParams) > 0) {
if ($multipart) {
$multipartContents = [];
foreach ($formParams as $formParamName => $formParamValue) {
$formParamValueItems = is_array($formParamValue) ? $formParamValue : [$formParamValue];
foreach ($formParamValueItems as $formParamValueItem) {
$multipartContents[] = [
'name' => $formParamName,
'contents' => $formParamValueItem
];
}
}
// for HTTP post (form)
$httpBody = new MultipartStream($multipartContents);
} elseif ($headers['Content-Type'] === 'application/json') {
$httpBody = \GuzzleHttp\json_encode($formParams);
} else {
// for HTTP post (form)
$httpBody = \GuzzleHttp\Psr7\Query::build($formParams);
}
}
// this endpoint requires API key authentication
$apiKey = $this->config->getApiKeyWithPrefix('hapikey');
if ($apiKey !== null) {
$queryParams['hapikey'] = $apiKey;
}
// this endpoint requires OAuth (access token)
if ($this->config->getAccessToken() !== null) {
$headers['Authorization'] = 'Bearer ' . $this->config->getAccessToken();
}
$defaultHeaders = [];
if ($this->config->getUserAgent()) {
$defaultHeaders['User-Agent'] = $this->config->getUserAgent();
}
$headers = array_merge(
$defaultHeaders,
$headerParams,
$headers
);
$query = \GuzzleHttp\Psr7\Query::build($queryParams);
return new Request(
'GET',
$this->config->getHost() . $resourcePath . ($query ? "?{$query}" : ''),
$headers,
$httpBody
);
}
/**
* Operation getPage
*
* List
*
* @param int $limit The maximum number of results to display per page. (optional, default to 10)
* @param string $after The paging cursor token of the last successfully read resource will be returned as the `paging.next.after` JSON property of a paged response containing more results. (optional)
* @param string[] $properties A comma separated list of the properties to be returned in the response. If any of the specified properties are not present on the requested object(s), they will be ignored. (optional)
* @param string[] $associations A comma separated list of object types to retrieve associated IDs for. If any of the specified associations do not exist, they will be ignored. (optional)
* @param bool $archived Whether to return only results that have been archived. (optional, default to false)
*
* @throws \HubSpot\Client\Crm\Deals\ApiException on non-2xx response
* @throws \InvalidArgumentException
* @return \HubSpot\Client\Crm\Deals\Model\CollectionResponseSimplePublicObjectWithAssociationsForwardPaging|\HubSpot\Client\Crm\Deals\Model\Error
*/
public function getPage($limit = 10, $after = null, $properties = null, $associations = null, $archived = false)
{
list($response) = $this->getPageWithHttpInfo($limit, $after, $properties, $associations, $archived);
return $response;
}
/**
* Operation getPageWithHttpInfo
*
* List
*
* @param int $limit The maximum number of results to display per page. (optional, default to 10)
* @param string $after The paging cursor token of the last successfully read resource will be returned as the `paging.next.after` JSON property of a paged response containing more results. (optional)
* @param string[] $properties A comma separated list of the properties to be returned in the response. If any of the specified properties are not present on the requested object(s), they will be ignored. (optional)
* @param string[] $associations A comma separated list of object types to retrieve associated IDs for. If any of the specified associations do not exist, they will be ignored. (optional)
* @param bool $archived Whether to return only results that have been archived. (optional, default to false)
*
* @throws \HubSpot\Client\Crm\Deals\ApiException on non-2xx response
* @throws \InvalidArgumentException
* @return array of \HubSpot\Client\Crm\Deals\Model\CollectionResponseSimplePublicObjectWithAssociationsForwardPaging|\HubSpot\Client\Crm\Deals\Model\Error, HTTP status code, HTTP response headers (array of strings)
*/
public function getPageWithHttpInfo($limit = 10, $after = null, $properties = null, $associations = null, $archived = false)
{
$request = $this->getPageRequest($limit, $after, $properties, $associations, $archived);
try {
$options = $this->createHttpClientOption();
try {
$response = $this->client->send($request, $options);
} catch (RequestException $e) {
throw new ApiException(
"[{$e->getCode()}] {$e->getMessage()}",
(int) $e->getCode(),
$e->getResponse() ? $e->getResponse()->getHeaders() : null,
$e->getResponse() ? (string) $e->getResponse()->getBody() : null
);
} catch (ConnectException $e) {
throw new ApiException(
"[{$e->getCode()}] {$e->getMessage()}",
(int) $e->getCode(),
null,
null
);
}
$statusCode = $response->getStatusCode();
if ($statusCode < 200 || $statusCode > 299) {
throw new ApiException(
sprintf(
'[%d] Error connecting to the API (%s)',
$statusCode,
(string) $request->getUri()
),
$statusCode,
$response->getHeaders(),
(string) $response->getBody()
);
}
switch($statusCode) {
case 200:
if ('\HubSpot\Client\Crm\Deals\Model\CollectionResponseSimplePublicObjectWithAssociationsForwardPaging' === '\SplFileObject') {
$content = $response->getBody(); //stream goes to serializer
} else {
$content = (string) $response->getBody();
}
return [
ObjectSerializer::deserialize($content, '\HubSpot\Client\Crm\Deals\Model\CollectionResponseSimplePublicObjectWithAssociationsForwardPaging', []),
$response->getStatusCode(),
$response->getHeaders()
];
default:
if ('\HubSpot\Client\Crm\Deals\Model\Error' === '\SplFileObject') {
$content = $response->getBody(); //stream goes to serializer
} else {
$content = (string) $response->getBody();
}
return [
ObjectSerializer::deserialize($content, '\HubSpot\Client\Crm\Deals\Model\Error', []),
$response->getStatusCode(),
$response->getHeaders()
];
}
$returnType = '\HubSpot\Client\Crm\Deals\Model\CollectionResponseSimplePublicObjectWithAssociationsForwardPaging';
if ($returnType === '\SplFileObject') {
$content = $response->getBody(); //stream goes to serializer
} else {
$content = (string) $response->getBody();
}
return [
ObjectSerializer::deserialize($content, $returnType, []),
$response->getStatusCode(),
$response->getHeaders()
];
} catch (ApiException $e) {
switch ($e->getCode()) {
case 200:
$data = ObjectSerializer::deserialize(
$e->getResponseBody(),
'\HubSpot\Client\Crm\Deals\Model\CollectionResponseSimplePublicObjectWithAssociationsForwardPaging',
$e->getResponseHeaders()
);
$e->setResponseObject($data);
break;
default:
$data = ObjectSerializer::deserialize(
$e->getResponseBody(),
'\HubSpot\Client\Crm\Deals\Model\Error',
$e->getResponseHeaders()
);
$e->setResponseObject($data);
break;
}
throw $e;
}
}
/**
* Operation getPageAsync
*
* List
*
* @param int $limit The maximum number of results to display per page. (optional, default to 10)
* @param string $after The paging cursor token of the last successfully read resource will be returned as the `paging.next.after` JSON property of a paged response containing more results. (optional)
* @param string[] $properties A comma separated list of the properties to be returned in the response. If any of the specified properties are not present on the requested object(s), they will be ignored. (optional)
* @param string[] $associations A comma separated list of object types to retrieve associated IDs for. If any of the specified associations do not exist, they will be ignored. (optional)
* @param bool $archived Whether to return only results that have been archived. (optional, default to false)
*
* @throws \InvalidArgumentException
* @return \GuzzleHttp\Promise\PromiseInterface
*/
public function getPageAsync($limit = 10, $after = null, $properties = null, $associations = null, $archived = false)
{
return $this->getPageAsyncWithHttpInfo($limit, $after, $properties, $associations, $archived)
->then(
function ($response) {
return $response[0];
}
);
}
/**
* Operation getPageAsyncWithHttpInfo
*
* List
*
* @param int $limit The maximum number of results to display per page. (optional, default to 10)
* @param string $after The paging cursor token of the last successfully read resource will be returned as the `paging.next.after` JSON property of a paged response containing more results. (optional)
* @param string[] $properties A comma separated list of the properties to be returned in the response. If any of the specified properties are not present on the requested object(s), they will be ignored. (optional)
* @param string[] $associations A comma separated list of object types to retrieve associated IDs for. If any of the specified associations do not exist, they will be ignored. (optional)
* @param bool $archived Whether to return only results that have been archived. (optional, default to false)
*
* @throws \InvalidArgumentException
* @return \GuzzleHttp\Promise\PromiseInterface
*/
public function getPageAsyncWithHttpInfo($limit = 10, $after = null, $properties = null, $associations = null, $archived = false)
{
$returnType = '\HubSpot\Client\Crm\Deals\Model\CollectionResponseSimplePublicObjectWithAssociationsForwardPaging';
$request = $this->getPageRequest($limit, $after, $properties, $associations, $archived);
return $this->client
->sendAsync($request, $this->createHttpClientOption())
->then(
function ($response) use ($returnType) {
if ($returnType === '\SplFileObject') {
$content = $response->getBody(); //stream goes to serializer
} else {
$content = (string) $response->getBody();
}
return [
ObjectSerializer::deserialize($content, $returnType, []),
$response->getStatusCode(),
$response->getHeaders()
];
},
function ($exception) {
$response = $exception->getResponse();
$statusCode = $response->getStatusCode();
throw new ApiException(
sprintf(
'[%d] Error connecting to the API (%s)',
$statusCode,
$exception->getRequest()->getUri()
),
$statusCode,
$response->getHeaders(),
(string) $response->getBody()
);
}
);
}
/**
* Create request for operation 'getPage'
*
* @param int $limit The maximum number of results to display per page. (optional, default to 10)
* @param string $after The paging cursor token of the last successfully read resource will be returned as the `paging.next.after` JSON property of a paged response containing more results. (optional)
* @param string[] $properties A comma separated list of the properties to be returned in the response. If any of the specified properties are not present on the requested object(s), they will be ignored. (optional)
* @param string[] $associations A comma separated list of object types to retrieve associated IDs for. If any of the specified associations do not exist, they will be ignored. (optional)
* @param bool $archived Whether to return only results that have been archived. (optional, default to false)
*
* @throws \InvalidArgumentException
* @return \GuzzleHttp\Psr7\Request
*/
public function getPageRequest($limit = 10, $after = null, $properties = null, $associations = null, $archived = false)
{
$res...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"on_screen":true,"help_text":"Git Branch: master","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"Editor for custom.log","depth":4,"on_screen":true,"role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Reader Mode","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Analyzing…","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXTextArea","text":"<?php\n/**\n * BasicApi\n * PHP version 7.3\n *\n * @category Class\n * @package HubSpot\\Client\\Crm\\Deals\n * @author OpenAPI Generator team\n * @link https://openapi-generator.tech\n */\n\n/**\n * Deals\n *\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: v3\n * Generated by: https://openapi-generator.tech\n * OpenAPI Generator version: 5.3.0\n */\n\n/**\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\nnamespace HubSpot\\Client\\Crm\\Deals\\Api;\n\nuse GuzzleHttp\\Client;\nuse GuzzleHttp\\ClientInterface;\nuse GuzzleHttp\\Exception\\RequestException;\nuse GuzzleHttp\\Exception\\ConnectException;\nuse GuzzleHttp\\Psr7\\MultipartStream;\nuse GuzzleHttp\\Psr7\\Request;\nuse GuzzleHttp\\RequestOptions;\nuse HubSpot\\Client\\Crm\\Deals\\ApiException;\nuse HubSpot\\Client\\Crm\\Deals\\Configuration;\nuse HubSpot\\Client\\Crm\\Deals\\HeaderSelector;\nuse HubSpot\\Client\\Crm\\Deals\\ObjectSerializer;\n\n/**\n * BasicApi Class Doc Comment\n *\n * @category Class\n * @package HubSpot\\Client\\Crm\\Deals\n * @author OpenAPI Generator team\n * @link https://openapi-generator.tech\n */\nclass BasicApi\n{\n /**\n * @var ClientInterface\n */\n protected $client;\n\n /**\n * @var Configuration\n */\n protected $config;\n\n /**\n * @var HeaderSelector\n */\n protected $headerSelector;\n\n /**\n * @var int Host index\n */\n protected $hostIndex;\n\n /**\n * @param ClientInterface $client\n * @param Configuration $config\n * @param HeaderSelector $selector\n * @param int $hostIndex (Optional) host index to select the list of hosts if defined in the OpenAPI spec\n */\n public function __construct(\n ClientInterface $client = null,\n Configuration $config = null,\n HeaderSelector $selector = null,\n $hostIndex = 0\n ) {\n $this->client = $client ?: new Client();\n $this->config = $config ?: new Configuration();\n $this->headerSelector = $selector ?: new HeaderSelector();\n $this->hostIndex = $hostIndex;\n }\n\n /**\n * Set the host index\n *\n * @param int $hostIndex Host index (required)\n */\n public function setHostIndex($hostIndex): void\n {\n $this->hostIndex = $hostIndex;\n }\n\n /**\n * Get the host index\n *\n * @return int Host index\n */\n public function getHostIndex()\n {\n return $this->hostIndex;\n }\n\n /**\n * @return Configuration\n */\n public function getConfig()\n {\n return $this->config;\n }\n\n /**\n * Operation archive\n *\n * Archive\n *\n * @param string $deal_id deal_id (required)\n *\n * @throws \\HubSpot\\Client\\Crm\\Deals\\ApiException on non-2xx response\n * @throws \\InvalidArgumentException\n * @return void\n */\n public function archive($deal_id)\n {\n $this->archiveWithHttpInfo($deal_id);\n }\n\n /**\n * Operation archiveWithHttpInfo\n *\n * Archive\n *\n * @param string $deal_id (required)\n *\n * @throws \\HubSpot\\Client\\Crm\\Deals\\ApiException on non-2xx response\n * @throws \\InvalidArgumentException\n * @return array of null, HTTP status code, HTTP response headers (array of strings)\n */\n public function archiveWithHttpInfo($deal_id)\n {\n $request = $this->archiveRequest($deal_id);\n\n try {\n $options = $this->createHttpClientOption();\n try {\n $response = $this->client->send($request, $options);\n } catch (RequestException $e) {\n throw new ApiException(\n \"[{$e->getCode()}] {$e->getMessage()}\",\n (int) $e->getCode(),\n $e->getResponse() ? $e->getResponse()->getHeaders() : null,\n $e->getResponse() ? (string) $e->getResponse()->getBody() : null\n );\n } catch (ConnectException $e) {\n throw new ApiException(\n \"[{$e->getCode()}] {$e->getMessage()}\",\n (int) $e->getCode(),\n null,\n null\n );\n }\n\n $statusCode = $response->getStatusCode();\n\n if ($statusCode < 200 || $statusCode > 299) {\n throw new ApiException(\n sprintf(\n '[%d] Error connecting to the API (%s)',\n $statusCode,\n (string) $request->getUri()\n ),\n $statusCode,\n $response->getHeaders(),\n (string) $response->getBody()\n );\n }\n\n return [null, $statusCode, $response->getHeaders()];\n\n } catch (ApiException $e) {\n switch ($e->getCode()) {\n default:\n $data = ObjectSerializer::deserialize(\n $e->getResponseBody(),\n '\\HubSpot\\Client\\Crm\\Deals\\Model\\Error',\n $e->getResponseHeaders()\n );\n $e->setResponseObject($data);\n break;\n }\n throw $e;\n }\n }\n\n /**\n * Operation archiveAsync\n *\n * Archive\n *\n * @param string $deal_id (required)\n *\n * @throws \\InvalidArgumentException\n * @return \\GuzzleHttp\\Promise\\PromiseInterface\n */\n public function archiveAsync($deal_id)\n {\n return $this->archiveAsyncWithHttpInfo($deal_id)\n ->then(\n function ($response) {\n return $response[0];\n }\n );\n }\n\n /**\n * Operation archiveAsyncWithHttpInfo\n *\n * Archive\n *\n * @param string $deal_id (required)\n *\n * @throws \\InvalidArgumentException\n * @return \\GuzzleHttp\\Promise\\PromiseInterface\n */\n public function archiveAsyncWithHttpInfo($deal_id)\n {\n $returnType = '';\n $request = $this->archiveRequest($deal_id);\n\n return $this->client\n ->sendAsync($request, $this->createHttpClientOption())\n ->then(\n function ($response) use ($returnType) {\n return [null, $response->getStatusCode(), $response->getHeaders()];\n },\n function ($exception) {\n $response = $exception->getResponse();\n $statusCode = $response->getStatusCode();\n throw new ApiException(\n sprintf(\n '[%d] Error connecting to the API (%s)',\n $statusCode,\n $exception->getRequest()->getUri()\n ),\n $statusCode,\n $response->getHeaders(),\n (string) $response->getBody()\n );\n }\n );\n }\n\n /**\n * Create request for operation 'archive'\n *\n * @param string $deal_id (required)\n *\n * @throws \\InvalidArgumentException\n * @return \\GuzzleHttp\\Psr7\\Request\n */\n public function archiveRequest($deal_id)\n {\n // verify the required parameter 'deal_id' is set\n if ($deal_id === null || (is_array($deal_id) && count($deal_id) === 0)) {\n throw new \\InvalidArgumentException(\n 'Missing the required parameter $deal_id when calling archive'\n );\n }\n\n $resourcePath = '/crm/v3/objects/deals/{dealId}';\n $formParams = [];\n $queryParams = [];\n $headerParams = [];\n $httpBody = '';\n $multipart = false;\n\n\n\n // path params\n if ($deal_id !== null) {\n $resourcePath = str_replace(\n '{' . 'dealId' . '}',\n ObjectSerializer::toPathValue($deal_id),\n $resourcePath\n );\n }\n\n\n if ($multipart) {\n $headers = $this->headerSelector->selectHeadersForMultipart(\n ['*/*']\n );\n } else {\n $headers = $this->headerSelector->selectHeaders(\n ['*/*'],\n []\n );\n }\n\n // for model (json/xml)\n if (count($formParams) > 0) {\n if ($multipart) {\n $multipartContents = [];\n foreach ($formParams as $formParamName => $formParamValue) {\n $formParamValueItems = is_array($formParamValue) ? $formParamValue : [$formParamValue];\n foreach ($formParamValueItems as $formParamValueItem) {\n $multipartContents[] = [\n 'name' => $formParamName,\n 'contents' => $formParamValueItem\n ];\n }\n }\n // for HTTP post (form)\n $httpBody = new MultipartStream($multipartContents);\n\n } elseif ($headers['Content-Type'] === 'application/json') {\n $httpBody = \\GuzzleHttp\\json_encode($formParams);\n\n } else {\n // for HTTP post (form)\n $httpBody = \\GuzzleHttp\\Psr7\\Query::build($formParams);\n }\n }\n\n // this endpoint requires API key authentication\n $apiKey = $this->config->getApiKeyWithPrefix('hapikey');\n if ($apiKey !== null) {\n $queryParams['hapikey'] = $apiKey;\n }\n // this endpoint requires OAuth (access token)\n if ($this->config->getAccessToken() !== null) {\n $headers['Authorization'] = 'Bearer ' . $this->config->getAccessToken();\n }\n\n $defaultHeaders = [];\n if ($this->config->getUserAgent()) {\n $defaultHeaders['User-Agent'] = $this->config->getUserAgent();\n }\n\n $headers = array_merge(\n $defaultHeaders,\n $headerParams,\n $headers\n );\n\n $query = \\GuzzleHttp\\Psr7\\Query::build($queryParams);\n return new Request(\n 'DELETE',\n $this->config->getHost() . $resourcePath . ($query ? \"?{$query}\" : ''),\n $headers,\n $httpBody\n );\n }\n\n /**\n * Operation create\n *\n * Create\n *\n * @param \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectInput $simple_public_object_input simple_public_object_input (required)\n *\n * @throws \\HubSpot\\Client\\Crm\\Deals\\ApiException on non-2xx response\n * @throws \\InvalidArgumentException\n * @return \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObject|\\HubSpot\\Client\\Crm\\Deals\\Model\\Error\n */\n public function create($simple_public_object_input)\n {\n list($response) = $this->createWithHttpInfo($simple_public_object_input);\n return $response;\n }\n\n /**\n * Operation createWithHttpInfo\n *\n * Create\n *\n * @param \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectInput $simple_public_object_input (required)\n *\n * @throws \\HubSpot\\Client\\Crm\\Deals\\ApiException on non-2xx response\n * @throws \\InvalidArgumentException\n * @return array of \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObject|\\HubSpot\\Client\\Crm\\Deals\\Model\\Error, HTTP status code, HTTP response headers (array of strings)\n */\n public function createWithHttpInfo($simple_public_object_input)\n {\n $request = $this->createRequest($simple_public_object_input);\n\n try {\n $options = $this->createHttpClientOption();\n try {\n $response = $this->client->send($request, $options);\n } catch (RequestException $e) {\n throw new ApiException(\n \"[{$e->getCode()}] {$e->getMessage()}\",\n (int) $e->getCode(),\n $e->getResponse() ? $e->getResponse()->getHeaders() : null,\n $e->getResponse() ? (string) $e->getResponse()->getBody() : null\n );\n } catch (ConnectException $e) {\n throw new ApiException(\n \"[{$e->getCode()}] {$e->getMessage()}\",\n (int) $e->getCode(),\n null,\n null\n );\n }\n\n $statusCode = $response->getStatusCode();\n\n if ($statusCode < 200 || $statusCode > 299) {\n throw new ApiException(\n sprintf(\n '[%d] Error connecting to the API (%s)',\n $statusCode,\n (string) $request->getUri()\n ),\n $statusCode,\n $response->getHeaders(),\n (string) $response->getBody()\n );\n }\n\n switch($statusCode) {\n case 201:\n if ('\\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObject' === '\\SplFileObject') {\n $content = $response->getBody(); //stream goes to serializer\n } else {\n $content = (string) $response->getBody();\n }\n\n return [\n ObjectSerializer::deserialize($content, '\\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObject', []),\n $response->getStatusCode(),\n $response->getHeaders()\n ];\n default:\n if ('\\HubSpot\\Client\\Crm\\Deals\\Model\\Error' === '\\SplFileObject') {\n $content = $response->getBody(); //stream goes to serializer\n } else {\n $content = (string) $response->getBody();\n }\n\n return [\n ObjectSerializer::deserialize($content, '\\HubSpot\\Client\\Crm\\Deals\\Model\\Error', []),\n $response->getStatusCode(),\n $response->getHeaders()\n ];\n }\n\n $returnType = '\\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObject';\n if ($returnType === '\\SplFileObject') {\n $content = $response->getBody(); //stream goes to serializer\n } else {\n $content = (string) $response->getBody();\n }\n\n return [\n ObjectSerializer::deserialize($content, $returnType, []),\n $response->getStatusCode(),\n $response->getHeaders()\n ];\n\n } catch (ApiException $e) {\n switch ($e->getCode()) {\n case 201:\n $data = ObjectSerializer::deserialize(\n $e->getResponseBody(),\n '\\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObject',\n $e->getResponseHeaders()\n );\n $e->setResponseObject($data);\n break;\n default:\n $data = ObjectSerializer::deserialize(\n $e->getResponseBody(),\n '\\HubSpot\\Client\\Crm\\Deals\\Model\\Error',\n $e->getResponseHeaders()\n );\n $e->setResponseObject($data);\n break;\n }\n throw $e;\n }\n }\n\n /**\n * Operation createAsync\n *\n * Create\n *\n * @param \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectInput $simple_public_object_input (required)\n *\n * @throws \\InvalidArgumentException\n * @return \\GuzzleHttp\\Promise\\PromiseInterface\n */\n public function createAsync($simple_public_object_input)\n {\n return $this->createAsyncWithHttpInfo($simple_public_object_input)\n ->then(\n function ($response) {\n return $response[0];\n }\n );\n }\n\n /**\n * Operation createAsyncWithHttpInfo\n *\n * Create\n *\n * @param \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectInput $simple_public_object_input (required)\n *\n * @throws \\InvalidArgumentException\n * @return \\GuzzleHttp\\Promise\\PromiseInterface\n */\n public function createAsyncWithHttpInfo($simple_public_object_input)\n {\n $returnType = '\\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObject';\n $request = $this->createRequest($simple_public_object_input);\n\n return $this->client\n ->sendAsync($request, $this->createHttpClientOption())\n ->then(\n function ($response) use ($returnType) {\n if ($returnType === '\\SplFileObject') {\n $content = $response->getBody(); //stream goes to serializer\n } else {\n $content = (string) $response->getBody();\n }\n\n return [\n ObjectSerializer::deserialize($content, $returnType, []),\n $response->getStatusCode(),\n $response->getHeaders()\n ];\n },\n function ($exception) {\n $response = $exception->getResponse();\n $statusCode = $response->getStatusCode();\n throw new ApiException(\n sprintf(\n '[%d] Error connecting to the API (%s)',\n $statusCode,\n $exception->getRequest()->getUri()\n ),\n $statusCode,\n $response->getHeaders(),\n (string) $response->getBody()\n );\n }\n );\n }\n\n /**\n * Create request for operation 'create'\n *\n * @param \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectInput $simple_public_object_input (required)\n *\n * @throws \\InvalidArgumentException\n * @return \\GuzzleHttp\\Psr7\\Request\n */\n public function createRequest($simple_public_object_input)\n {\n // verify the required parameter 'simple_public_object_input' is set\n if ($simple_public_object_input === null || (is_array($simple_public_object_input) && count($simple_public_object_input) === 0)) {\n throw new \\InvalidArgumentException(\n 'Missing the required parameter $simple_public_object_input when calling create'\n );\n }\n\n $resourcePath = '/crm/v3/objects/deals';\n $formParams = [];\n $queryParams = [];\n $headerParams = [];\n $httpBody = '';\n $multipart = false;\n\n\n\n\n\n if ($multipart) {\n $headers = $this->headerSelector->selectHeadersForMultipart(\n ['application/json', '*/*']\n );\n } else {\n $headers = $this->headerSelector->selectHeaders(\n ['application/json', '*/*'],\n ['application/json']\n );\n }\n\n // for model (json/xml)\n if (isset($simple_public_object_input)) {\n if ($headers['Content-Type'] === 'application/json') {\n $httpBody = \\GuzzleHttp\\json_encode(ObjectSerializer::sanitizeForSerialization($simple_public_object_input));\n } else {\n $httpBody = $simple_public_object_input;\n }\n } elseif (count($formParams) > 0) {\n if ($multipart) {\n $multipartContents = [];\n foreach ($formParams as $formParamName => $formParamValue) {\n $formParamValueItems = is_array($formParamValue) ? $formParamValue : [$formParamValue];\n foreach ($formParamValueItems as $formParamValueItem) {\n $multipartContents[] = [\n 'name' => $formParamName,\n 'contents' => $formParamValueItem\n ];\n }\n }\n // for HTTP post (form)\n $httpBody = new MultipartStream($multipartContents);\n\n } elseif ($headers['Content-Type'] === 'application/json') {\n $httpBody = \\GuzzleHttp\\json_encode($formParams);\n\n } else {\n // for HTTP post (form)\n $httpBody = \\GuzzleHttp\\Psr7\\Query::build($formParams);\n }\n }\n\n // this endpoint requires API key authentication\n $apiKey = $this->config->getApiKeyWithPrefix('hapikey');\n if ($apiKey !== null) {\n $queryParams['hapikey'] = $apiKey;\n }\n // this endpoint requires OAuth (access token)\n if ($this->config->getAccessToken() !== null) {\n $headers['Authorization'] = 'Bearer ' . $this->config->getAccessToken();\n }\n\n $defaultHeaders = [];\n if ($this->config->getUserAgent()) {\n $defaultHeaders['User-Agent'] = $this->config->getUserAgent();\n }\n\n $headers = array_merge(\n $defaultHeaders,\n $headerParams,\n $headers\n );\n\n $query = \\GuzzleHttp\\Psr7\\Query::build($queryParams);\n return new Request(\n 'POST',\n $this->config->getHost() . $resourcePath . ($query ? \"?{$query}\" : ''),\n $headers,\n $httpBody\n );\n }\n\n /**\n * Operation getById\n *\n * Read\n *\n * @param string $deal_id deal_id (required)\n * @param string[] $properties A comma separated list of the properties to be returned in the response. If any of the specified properties are not present on the requested object(s), they will be ignored. (optional)\n * @param string[] $associations A comma separated list of object types to retrieve associated IDs for. If any of the specified associations do not exist, they will be ignored. (optional)\n * @param bool $archived Whether to return only results that have been archived. (optional, default to false)\n * @param string $id_property The name of a property whose values are unique for this object type (optional)\n *\n * @throws \\HubSpot\\Client\\Crm\\Deals\\ApiException on non-2xx response\n * @throws \\InvalidArgumentException\n * @return \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations|\\HubSpot\\Client\\Crm\\Deals\\Model\\Error\n */\n public function getById($deal_id, $properties = null, $associations = null, $archived = false, $id_property = null)\n {\n list($response) = $this->getByIdWithHttpInfo($deal_id, $properties, $associations, $archived, $id_property);\n return $response;\n }\n\n /**\n * Operation getByIdWithHttpInfo\n *\n * Read\n *\n * @param string $deal_id (required)\n * @param string[] $properties A comma separated list of the properties to be returned in the response. If any of the specified properties are not present on the requested object(s), they will be ignored. (optional)\n * @param string[] $associations A comma separated list of object types to retrieve associated IDs for. If any of the specified associations do not exist, they will be ignored. (optional)\n * @param bool $archived Whether to return only results that have been archived. (optional, default to false)\n * @param string $id_property The name of a property whose values are unique for this object type (optional)\n *\n * @throws \\HubSpot\\Client\\Crm\\Deals\\ApiException on non-2xx response\n * @throws \\InvalidArgumentException\n * @return array of \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations|\\HubSpot\\Client\\Crm\\Deals\\Model\\Error, HTTP status code, HTTP response headers (array of strings)\n */\n public function getByIdWithHttpInfo($deal_id, $properties = null, $associations = null, $archived = false, $id_property = null)\n {\n $request = $this->getByIdRequest($deal_id, $properties, $associations, $archived, $id_property);\n\n try {\n $options = $this->createHttpClientOption();\n try {\n $response = $this->client->send($request, $options);\n } catch (RequestException $e) {\n throw new ApiException(\n \"[{$e->getCode()}] {$e->getMessage()}\",\n (int) $e->getCode(),\n $e->getResponse() ? $e->getResponse()->getHeaders() : null,\n $e->getResponse() ? (string) $e->getResponse()->getBody() : null\n );\n } catch (ConnectException $e) {\n throw new ApiException(\n \"[{$e->getCode()}] {$e->getMessage()}\",\n (int) $e->getCode(),\n null,\n null\n );\n }\n\n $statusCode = $response->getStatusCode();\n\n if ($statusCode < 200 || $statusCode > 299) {\n throw new ApiException(\n sprintf(\n '[%d] Error connecting to the API (%s)',\n $statusCode,\n (string) $request->getUri()\n ),\n $statusCode,\n $response->getHeaders(),\n (string) $response->getBody()\n );\n }\n\n switch($statusCode) {\n case 200:\n if ('\\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations' === '\\SplFileObject') {\n $content = $response->getBody(); //stream goes to serializer\n } else {\n $content = (string) $response->getBody();\n }\n\n return [\n ObjectSerializer::deserialize($content, '\\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations', []),\n $response->getStatusCode(),\n $response->getHeaders()\n ];\n default:\n if ('\\HubSpot\\Client\\Crm\\Deals\\Model\\Error' === '\\SplFileObject') {\n $content = $response->getBody(); //stream goes to serializer\n } else {\n $content = (string) $response->getBody();\n }\n\n return [\n ObjectSerializer::deserialize($content, '\\HubSpot\\Client\\Crm\\Deals\\Model\\Error', []),\n $response->getStatusCode(),\n $response->getHeaders()\n ];\n }\n\n $returnType = '\\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations';\n if ($returnType === '\\SplFileObject') {\n $content = $response->getBody(); //stream goes to serializer\n } else {\n $content = (string) $response->getBody();\n }\n\n return [\n ObjectSerializer::deserialize($content, $returnType, []),\n $response->getStatusCode(),\n $response->getHeaders()\n ];\n\n } catch (ApiException $e) {\n switch ($e->getCode()) {\n case 200:\n $data = ObjectSerializer::deserialize(\n $e->getResponseBody(),\n '\\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations',\n $e->getResponseHeaders()\n );\n $e->setResponseObject($data);\n break;\n default:\n $data = ObjectSerializer::deserialize(\n $e->getResponseBody(),\n '\\HubSpot\\Client\\Crm\\Deals\\Model\\Error',\n $e->getResponseHeaders()\n );\n $e->setResponseObject($data);\n break;\n }\n throw $e;\n }\n }\n\n /**\n * Operation getByIdAsync\n *\n * Read\n *\n * @param string $deal_id (required)\n * @param string[] $properties A comma separated list of the properties to be returned in the response. If any of the specified properties are not present on the requested object(s), they will be ignored. (optional)\n * @param string[] $associations A comma separated list of object types to retrieve associated IDs for. If any of the specified associations do not exist, they will be ignored. (optional)\n * @param bool $archived Whether to return only results that have been archived. (optional, default to false)\n * @param string $id_property The name of a property whose values are unique for this object type (optional)\n *\n * @throws \\InvalidArgumentException\n * @return \\GuzzleHttp\\Promise\\PromiseInterface\n */\n public function getByIdAsync($deal_id, $properties = null, $associations = null, $archived = false, $id_property = null)\n {\n return $this->getByIdAsyncWithHttpInfo($deal_id, $properties, $associations, $archived, $id_property)\n ->then(\n function ($response) {\n return $response[0];\n }\n );\n }\n\n /**\n * Operation getByIdAsyncWithHttpInfo\n *\n * Read\n *\n * @param string $deal_id (required)\n * @param string[] $properties A comma separated list of the properties to be returned in the response. If any of the specified properties are not present on the requested object(s), they will be ignored. (optional)\n * @param string[] $associations A comma separated list of object types to retrieve associated IDs for. If any of the specified associations do not exist, they will be ignored. (optional)\n * @param bool $archived Whether to return only results that have been archived. (optional, default to false)\n * @param string $id_property The name of a property whose values are unique for this object type (optional)\n *\n * @throws \\InvalidArgumentException\n * @return \\GuzzleHttp\\Promise\\PromiseInterface\n */\n public function getByIdAsyncWithHttpInfo($deal_id, $properties = null, $associations = null, $archived = false, $id_property = null)\n {\n $returnType = '\\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations';\n $request = $this->getByIdRequest($deal_id, $properties, $associations, $archived, $id_property);\n\n return $this->client\n ->sendAsync($request, $this->createHttpClientOption())\n ->then(\n function ($response) use ($returnType) {\n if ($returnType === '\\SplFileObject') {\n $content = $response->getBody(); //stream goes to serializer\n } else {\n $content = (string) $response->getBody();\n }\n\n return [\n ObjectSerializer::deserialize($content, $returnType, []),\n $response->getStatusCode(),\n $response->getHeaders()\n ];\n },\n function ($exception) {\n $response = $exception->getResponse();\n $statusCode = $response->getStatusCode();\n throw new ApiException(\n sprintf(\n '[%d] Error connecting to the API (%s)',\n $statusCode,\n $exception->getRequest()->getUri()\n ),\n $statusCode,\n $response->getHeaders(),\n (string) $response->getBody()\n );\n }\n );\n }\n\n /**\n * Create request for operation 'getById'\n *\n * @param string $deal_id (required)\n * @param string[] $properties A comma separated list of the properties to be returned in the response. If any of the specified properties are not present on the requested object(s), they will be ignored. (optional)\n * @param string[] $associations A comma separated list of object types to retrieve associated IDs for. If any of the specified associations do not exist, they will be ignored. (optional)\n * @param bool $archived Whether to return only results that have been archived. (optional, default to false)\n * @param string $id_property The name of a property whose values are unique for this object type (optional)\n *\n * @throws \\InvalidArgumentException\n * @return \\GuzzleHttp\\Psr7\\Request\n */\n public function getByIdRequest($deal_id, $properties = null, $associations = null, $archived = false, $id_property = null)\n {\n // verify the required parameter 'deal_id' is set\n if ($deal_id === null || (is_array($deal_id) && count($deal_id) === 0)) {\n throw new \\InvalidArgumentException(\n 'Missing the required parameter $deal_id when calling getById'\n );\n }\n\n $resourcePath = '/crm/v3/objects/deals/{dealId}';\n $formParams = [];\n $queryParams = [];\n $headerParams = [];\n $httpBody = '';\n $multipart = false;\n\n // query params\n if ($properties !== null) {\n if('form' === 'form' && is_array($properties)) {\n foreach($properties as $key => $value) {\n $queryParams[$key] = $value;\n }\n }\n else {\n $queryParams['properties'] = $properties;\n }\n }\n // query params\n if ($associations !== null) {\n if('form' === 'form' && is_array($associations)) {\n foreach($associations as $key => $value) {\n $queryParams[$key] = $value;\n }\n }\n else {\n $queryParams['associations'] = $associations;\n }\n }\n // query params\n if ($archived !== null) {\n if('form' === 'form' && is_array($archived)) {\n foreach($archived as $key => $value) {\n $queryParams[$key] = $value;\n }\n }\n else {\n $queryParams['archived'] = $archived;\n }\n }\n // query params\n if ($id_property !== null) {\n if('form' === 'form' && is_array($id_property)) {\n foreach($id_property as $key => $value) {\n $queryParams[$key] = $value;\n }\n }\n else {\n $queryParams['idProperty'] = $id_property;\n }\n }\n\n\n // path params\n if ($deal_id !== null) {\n $resourcePath = str_replace(\n '{' . 'dealId' . '}',\n ObjectSerializer::toPathValue($deal_id),\n $resourcePath\n );\n }\n\n\n if ($multipart) {\n $headers = $this->headerSelector->selectHeadersForMultipart(\n ['application/json', '*/*']\n );\n } else {\n $headers = $this->headerSelector->selectHeaders(\n ['application/json', '*/*'],\n []\n );\n }\n\n // for model (json/xml)\n if (count($formParams) > 0) {\n if ($multipart) {\n $multipartContents = [];\n foreach ($formParams as $formParamName => $formParamValue) {\n $formParamValueItems = is_array($formParamValue) ? $formParamValue : [$formParamValue];\n foreach ($formParamValueItems as $formParamValueItem) {\n $multipartContents[] = [\n 'name' => $formParamName,\n 'contents' => $formParamValueItem\n ];\n }\n }\n // for HTTP post (form)\n $httpBody = new MultipartStream($multipartContents);\n\n } elseif ($headers['Content-Type'] === 'application/json') {\n $httpBody = \\GuzzleHttp\\json_encode($formParams);\n\n } else {\n // for HTTP post (form)\n $httpBody = \\GuzzleHttp\\Psr7\\Query::build($formParams);\n }\n }\n\n // this endpoint requires API key authentication\n $apiKey = $this->config->getApiKeyWithPrefix('hapikey');\n if ($apiKey !== null) {\n $queryParams['hapikey'] = $apiKey;\n }\n // this endpoint requires OAuth (access token)\n if ($this->config->getAccessToken() !== null) {\n $headers['Authorization'] = 'Bearer ' . $this->config->getAccessToken();\n }\n\n $defaultHeaders = [];\n if ($this->config->getUserAgent()) {\n $defaultHeaders['User-Agent'] = $this->config->getUserAgent();\n }\n\n $headers = array_merge(\n $defaultHeaders,\n $headerParams,\n $headers\n );\n\n $query = \\GuzzleHttp\\Psr7\\Query::build($queryParams);\n return new Request(\n 'GET',\n $this->config->getHost() . $resourcePath . ($query ? \"?{$query}\" : ''),\n $headers,\n $httpBody\n );\n }\n\n /**\n * Operation getPage\n *\n * List\n *\n * @param int $limit The maximum number of results to display per page. (optional, default to 10)\n * @param string $after The paging cursor token of the last successfully read resource will be returned as the `paging.next.after` JSON property of a paged response containing more results. (optional)\n * @param string[] $properties A comma separated list of the properties to be returned in the response. If any of the specified properties are not present on the requested object(s), they will be ignored. (optional)\n * @param string[] $associations A comma separated list of object types to retrieve associated IDs for. If any of the specified associations do not exist, they will be ignored. (optional)\n * @param bool $archived Whether to return only results that have been archived. (optional, default to false)\n *\n * @throws \\HubSpot\\Client\\Crm\\Deals\\ApiException on non-2xx response\n * @throws \\InvalidArgumentException\n * @return \\HubSpot\\Client\\Crm\\Deals\\Model\\CollectionResponseSimplePublicObjectWithAssociationsForwardPaging|\\HubSpot\\Client\\Crm\\Deals\\Model\\Error\n */\n public function getPage($limit = 10, $after = null, $properties = null, $associations = null, $archived = false)\n {\n list($response) = $this->getPageWithHttpInfo($limit, $after, $properties, $associations, $archived);\n return $response;\n }\n\n /**\n * Operation getPageWithHttpInfo\n *\n * List\n *\n * @param int $limit The maximum number of results to display per page. (optional, default to 10)\n * @param string $after The paging cursor token of the last successfully read resource will be returned as the `paging.next.after` JSON property of a paged response containing more results. (optional)\n * @param string[] $properties A comma separated list of the properties to be returned in the response. If any of the specified properties are not present on the requested object(s), they will be ignored. (optional)\n * @param string[] $associations A comma separated list of object types to retrieve associated IDs for. If any of the specified associations do not exist, they will be ignored. (optional)\n * @param bool $archived Whether to return only results that have been archived. (optional, default to false)\n *\n * @throws \\HubSpot\\Client\\Crm\\Deals\\ApiException on non-2xx response\n * @throws \\InvalidArgumentException\n * @return array of \\HubSpot\\Client\\Crm\\Deals\\Model\\CollectionResponseSimplePublicObjectWithAssociationsForwardPaging|\\HubSpot\\Client\\Crm\\Deals\\Model\\Error, HTTP status code, HTTP response headers (array of strings)\n */\n public function getPageWithHttpInfo($limit = 10, $after = null, $properties = null, $associations = null, $archived = false)\n {\n $request = $this->getPageRequest($limit, $after, $properties, $associations, $archived);\n\n try {\n $options = $this->createHttpClientOption();\n try {\n $response = $this->client->send($request, $options);\n } catch (RequestException $e) {\n throw new ApiException(\n \"[{$e->getCode()}] {$e->getMessage()}\",\n (int) $e->getCode(),\n $e->getResponse() ? $e->getResponse()->getHeaders() : null,\n $e->getResponse() ? (string) $e->getResponse()->getBody() : null\n );\n } catch (ConnectException $e) {\n throw new ApiException(\n \"[{$e->getCode()}] {$e->getMessage()}\",\n (int) $e->getCode(),\n null,\n null\n );\n }\n\n $statusCode = $response->getStatusCode();\n\n if ($statusCode < 200 || $statusCode > 299) {\n throw new ApiException(\n sprintf(\n '[%d] Error connecting to the API (%s)',\n $statusCode,\n (string) $request->getUri()\n ),\n $statusCode,\n $response->getHeaders(),\n (string) $response->getBody()\n );\n }\n\n switch($statusCode) {\n case 200:\n if ('\\HubSpot\\Client\\Crm\\Deals\\Model\\CollectionResponseSimplePublicObjectWithAssociationsForwardPaging' === '\\SplFileObject') {\n $content = $response->getBody(); //stream goes to serializer\n } else {\n $content = (string) $response->getBody();\n }\n\n return [\n ObjectSerializer::deserialize($content, '\\HubSpot\\Client\\Crm\\Deals\\Model\\CollectionResponseSimplePublicObjectWithAssociationsForwardPaging', []),\n $response->getStatusCode(),\n $response->getHeaders()\n ];\n default:\n if ('\\HubSpot\\Client\\Crm\\Deals\\Model\\Error' === '\\SplFileObject') {\n $content = $response->getBody(); //stream goes to serializer\n } else {\n $content = (string) $response->getBody();\n }\n\n return [\n ObjectSerializer::deserialize($content, '\\HubSpot\\Client\\Crm\\Deals\\Model\\Error', []),\n $response->getStatusCode(),\n $response->getHeaders()\n ];\n }\n\n $returnType = '\\HubSpot\\Client\\Crm\\Deals\\Model\\CollectionResponseSimplePublicObjectWithAssociationsForwardPaging';\n if ($returnType === '\\SplFileObject') {\n $content = $response->getBody(); //stream goes to serializer\n } else {\n $content = (string) $response->getBody();\n }\n\n return [\n ObjectSerializer::deserialize($content, $returnType, []),\n $response->getStatusCode(),\n $response->getHeaders()\n ];\n\n } catch (ApiException $e) {\n switch ($e->getCode()) {\n case 200:\n $data = ObjectSerializer::deserialize(\n $e->getResponseBody(),\n '\\HubSpot\\Client\\Crm\\Deals\\Model\\CollectionResponseSimplePublicObjectWithAssociationsForwardPaging',\n $e->getResponseHeaders()\n );\n $e->setResponseObject($data);\n break;\n default:\n $data = ObjectSerializer::deserialize(\n $e->getResponseBody(),\n '\\HubSpot\\Client\\Crm\\Deals\\Model\\Error',\n $e->getResponseHeaders()\n );\n $e->setResponseObject($data);\n break;\n }\n throw $e;\n }\n }\n\n /**\n * Operation getPageAsync\n *\n * List\n *\n * @param int $limit The maximum number of results to display per page. (optional, default to 10)\n * @param string $after The paging cursor token of the last successfully read resource will be returned as the `paging.next.after` JSON property of a paged response containing more results. (optional)\n * @param string[] $properties A comma separated list of the properties to be returned in the response. If any of the specified properties are not present on the requested object(s), they will be ignored. (optional)\n * @param string[] $associations A comma separated list of object types to retrieve associated IDs for. If any of the specified associations do not exist, they will be ignored. (optional)\n * @param bool $archived Whether to return only results that have been archived. (optional, default to false)\n *\n * @throws \\InvalidArgumentException\n * @return \\GuzzleHttp\\Promise\\PromiseInterface\n */\n public function getPageAsync($limit = 10, $after = null, $properties = null, $associations = null, $archived = false)\n {\n return $this->getPageAsyncWithHttpInfo($limit, $after, $properties, $associations, $archived)\n ->then(\n function ($response) {\n return $response[0];\n }\n );\n }\n\n /**\n * Operation getPageAsyncWithHttpInfo\n *\n * List\n *\n * @param int $limit The maximum number of results to display per page. (optional, default to 10)\n * @param string $after The paging cursor token of the last successfully read resource will be returned as the `paging.next.after` JSON property of a paged response containing more results. (optional)\n * @param string[] $properties A comma separated list of the properties to be returned in the response. If any of the specified properties are not present on the requested object(s), they will be ignored. (optional)\n * @param string[] $associations A comma separated list of object types to retrieve associated IDs for. If any of the specified associations do not exist, they will be ignored. (optional)\n * @param bool $archived Whether to return only results that have been archived. (optional, default to false)\n *\n * @throws \\InvalidArgumentException\n * @return \\GuzzleHttp\\Promise\\PromiseInterface\n */\n public function getPageAsyncWithHttpInfo($limit = 10, $after = null, $properties = null, $associations = null, $archived = false)\n {\n $returnType = '\\HubSpot\\Client\\Crm\\Deals\\Model\\CollectionResponseSimplePublicObjectWithAssociationsForwardPaging';\n $request = $this->getPageRequest($limit, $after, $properties, $associations, $archived);\n\n return $this->client\n ->sendAsync($request, $this->createHttpClientOption())\n ->then(\n function ($response) use ($returnType) {\n if ($returnType === '\\SplFileObject') {\n $content = $response->getBody(); //stream goes to serializer\n } else {\n $content = (string) $response->getBody();\n }\n\n return [\n ObjectSerializer::deserialize($content, $returnType, []),\n $response->getStatusCode(),\n $response->getHeaders()\n ];\n },\n function ($exception) {\n $response = $exception->getResponse();\n $statusCode = $response->getStatusCode();\n throw new ApiException(\n sprintf(\n '[%d] Error connecting to the API (%s)',\n $statusCode,\n $exception->getRequest()->getUri()\n ),\n $statusCode,\n $response->getHeaders(),\n (string) $response->getBody()\n );\n }\n );\n }\n\n /**\n * Create request for operation 'getPage'\n *\n * @param int $limit The maximum number of results to display per page. (optional, default to 10)\n * @param string $after The paging cursor token of the last successfully read resource will be returned as the `paging.next.after` JSON property of a paged response containing more results. (optional)\n * @param string[] $properties A comma separated list of the properties to be returned in the response. If any of the specified properties are not present on the requested object(s), they will be ignored. (optional)\n * @param string[] $associations A comma separated list of object types to retrieve associated IDs for. If any of the specified associations do not exist, they will be ignored. (optional)\n * @param bool $archived Whether to return only results that have been archived. (optional, default to false)\n *\n * @throws \\InvalidArgumentException\n * @return \\GuzzleHttp\\Psr7\\Request\n */\n public function getPageRequest($limit = 10, $after = null, $properties = null, $associations = null, $archived = false)\n {\n\n $resourcePath = '/crm/v3/objects/deals';\n $formParams = [];\n $queryParams = [];\n $headerParams = [];\n $httpBody = '';\n $multipart = false;\n\n // query params\n if ($limit !== null) {\n if('form' === 'form' && is_array($limit)) {\n foreach($limit as $key => $value) {\n $queryParams[$key] = $value;\n }\n }\n else {\n $queryParams['limit'] = $limit;\n }\n }\n // query params\n if ($after !== null) {\n if('form' === 'form' && is_array($after)) {\n foreach($after as $key => $value) {\n $queryParams[$key] = $value;\n }\n }\n else {\n $queryParams['after'] = $after;\n }\n }\n // query params\n if ($properties !== null) {\n if('form' === 'form' && is_array($properties)) {\n foreach($properties as $key => $value) {\n $queryParams[$key] = $value;\n }\n }\n else {\n $queryParams['properties'] = $properties;\n }\n }\n // query params\n if ($associations !== null) {\n if('form' === 'form' && is_array($associations)) {\n foreach($associations as $key => $value) {\n $queryParams[$key] = $value;\n }\n }\n else {\n $queryParams['associations'] = $associations;\n }\n }\n // query params\n if ($archived !== null) {\n if('form' === 'form' && is_array($archived)) {\n foreach($archived as $key => $value) {\n $queryParams[$key] = $value;\n }\n }\n else {\n $queryParams['archived'] = $archived;\n }\n }\n\n\n\n\n if ($multipart) {\n $headers = $this->headerSelector->selectHeadersForMultipart(\n ['application/json', '*/*']\n );\n } else {\n $headers = $this->headerSelector->selectHeaders(\n ['application/json', '*/*'],\n []\n );\n }\n\n // for model (json/xml)\n if (count($formParams) > 0) {\n if ($multipart) {\n $multipartContents = [];\n foreach ($formParams as $formParamName => $formParamValue) {\n $formParamValueItems = is_array($formParamValue) ? $formParamValue : [$formParamValue];\n foreach ($formParamValueItems as $formParamValueItem) {\n $multipartContents[] = [\n 'name' => $formParamName,\n 'contents' => $formParamValueItem\n ];\n }\n }\n // for HTTP post (form)\n $httpBody = new MultipartStream($multipartContents);\n\n } elseif ($headers['Content-Type'] === 'application/json') {\n $httpBody = \\GuzzleHttp\\json_encode($formParams);\n\n } else {\n // for HTTP post (form)\n $httpBody = \\GuzzleHttp\\Psr7\\Query::build($formParams);\n }\n }\n\n // this endpoint requires API key authentication\n $apiKey = $this->config->getApiKeyWithPrefix('hapikey');\n if ($apiKey !== null) {\n $queryParams['hapikey'] = $apiKey;\n }\n // this endpoint requires OAuth (access token)\n if ($this->config->getAccessToken() !== null) {\n $headers['Authorization'] = 'Bearer ' . $this->config->getAccessToken();\n }\n\n $defaultHeaders = [];\n if ($this->config->getUserAgent()) {\n $defaultHeaders['User-Agent'] = $this->config->getUserAgent();\n }\n\n $headers = array_merge(\n $defaultHeaders,\n $headerParams,\n $headers\n );\n\n $query = \\GuzzleHttp\\Psr7\\Query::build($queryParams);\n return new Request(\n 'GET',\n $this->config->getHost() . $resourcePath . ($query ? \"?{$query}\" : ''),\n $headers,\n $httpBody\n );\n }\n\n /**\n * Operation update\n *\n * Update\n *\n * @param string $deal_id deal_id (required)\n * @param \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectInput $simple_public_object_input simple_public_object_input (required)\n * @param string $id_property The name of a property whose values are unique for this object type (optional)\n *\n * @throws \\HubSpot\\Client\\Crm\\Deals\\ApiException on non-2xx response\n * @throws \\InvalidArgumentException\n * @return \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObject|\\HubSpot\\Client\\Crm\\Deals\\Model\\Error\n */\n public function update($deal_id, $simple_public_object_input, $id_property = null)\n {\n list($response) = $this->updateWithHttpInfo($deal_id, $simple_public_object_input, $id_property);\n return $response;\n }\n\n /**\n * Operation updateWithHttpInfo\n *\n * Update\n *\n * @param string $deal_id (required)\n * @param \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectInput $simple_public_object_input (required)\n * @param string $id_property The name of a property whose values are unique for this object type (optional)\n *\n * @throws \\HubSpot\\Client\\Crm\\Deals\\ApiException on non-2xx response\n * @throws \\InvalidArgumentException\n * @return array of \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObject|\\HubSpot\\Client\\Crm\\Deals\\Model\\Error, HTTP status code, HTTP response headers (array of strings)\n */\n public function updateWithHttpInfo($deal_id, $simple_public_object_input, $id_property = null)\n {\n $request = $this->updateRequest($deal_id, $simple_public_object_input, $id_property);\n\n try {\n $options = $this->createHttpClientOption();\n try {\n $response = $this->client->send($request, $options);\n } catch (RequestException $e) {\n throw new ApiException(\n \"[{$e->getCode()}] {$e->getMessage()}\",\n (int) $e->getCode(),\n $e->getResponse() ? $e->getResponse()->getHeaders() : null,\n $e->getResponse() ? (string) $e->getResponse()->getBody() : null\n );\n } catch (ConnectException $e) {\n throw new ApiException(\n \"[{$e->getCode()}] {$e->getMessage()}\",\n (int) $e->getCode(),\n null,\n null\n );\n }\n\n $statusCode = $response->getStatusCode();\n\n if ($statusCode < 200 || $statusCode > 299) {\n throw new ApiException(\n sprintf(\n '[%d] Error connecting to the API (%s)',\n $statusCode,\n (string) $request->getUri()\n ),\n $statusCode,\n $response->getHeaders(),\n (string) $response->getBody()\n );\n }\n\n switch($statusCode) {\n case 200:\n if ('\\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObject' === '\\SplFileObject') {\n $content = $response->getBody(); //stream goes to serializer\n } else {\n $content = (string) $response->getBody();\n }\n\n return [\n ObjectSerializer::deserialize($content, '\\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObject', []),\n $response->getStatusCode(),\n $response->getHeaders()\n ];\n default:\n if ('\\HubSpot\\Client\\Crm\\Deals\\Model\\Error' === '\\SplFileObject') {\n $content = $response->getBody(); //stream goes to serializer\n } else {\n $content = (string) $response->getBody();\n }\n\n return [\n ObjectSerializer::deserialize($content, '\\HubSpot\\Client\\Crm\\Deals\\Model\\Error', []),\n $response->getStatusCode(),\n $response->getHeaders()\n ];\n }\n\n $returnType = '\\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObject';\n if ($returnType === '\\SplFileObject') {\n $content = $response->getBody(); //stream goes to serializer\n } else {\n $content = (string) $response->getBody();\n }\n\n return [\n ObjectSerializer::deserialize($content, $returnType, []),\n $response->getStatusCode(),\n $response->getHeaders()\n ];\n\n } catch (ApiException $e) {\n switch ($e->getCode()) {\n case 200:\n $data = ObjectSerializer::deserialize(\n $e->getResponseBody(),\n '\\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObject',\n $e->getResponseHeaders()\n );\n $e->setResponseObject($data);\n break;\n default:\n $data = ObjectSerializer::deserialize(\n $e->getResponseBody(),\n '\\HubSpot\\Client\\Crm\\Deals\\Model\\Error',\n $e->getResponseHeaders()\n );\n $e->setResponseObject($data);\n break;\n }\n throw $e;\n }\n }\n\n /**\n * Operation updateAsync\n *\n * Update\n *\n * @param string $deal_id (required)\n * @param \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectInput $simple_public_object_input (required)\n * @param string $id_property The name of a property whose values are unique for this object type (optional)\n *\n * @throws \\InvalidArgumentException\n * @return \\GuzzleHttp\\Promise\\PromiseInterface\n */\n public function updateAsync($deal_id, $simple_public_object_input, $id_property = null)\n {\n return $this->updateAsyncWithHttpInfo($deal_id, $simple_public_object_input, $id_property)\n ->then(\n function ($response) {\n return $response[0];\n }\n );\n }\n\n /**\n * Operation updateAsyncWithHttpInfo\n *\n * Update\n *\n * @param string $deal_id (required)\n * @param \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectInput $simple_public_object_input (required)\n * @param string $id_property The name of a property whose values are unique for this object type (optional)\n *\n * @throws \\InvalidArgumentException\n * @return \\GuzzleHttp\\Promise\\PromiseInterface\n */\n public function updateAsyncWithHttpInfo($deal_id, $simple_public_object_input, $id_property = null)\n {\n $returnType = '\\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObject';\n $request = $this->updateRequest($deal_id, $simple_public_object_input, $id_property);\n\n return $this->client\n ->sendAsync($request, $this->createHttpClientOption())\n ->then(\n function ($response) use ($returnType) {\n if ($returnType === '\\SplFileObject') {\n $content = $response->getBody(); //stream goes to serializer\n } else {\n $content = (string) $response->getBody();\n }\n\n return [\n ObjectSerializer::deserialize($content, $returnType, []),\n $response->getStatusCode(),\n $response->getHeaders()\n ];\n },\n function ($exception) {\n $response = $exception->getResponse();\n $statusCode = $response->getStatusCode();\n throw new ApiException(\n sprintf(\n '[%d] Error connecting to the API (%s)',\n $statusCode,\n $exception->getRequest()->getUri()\n ),\n $statusCode,\n $response->getHeaders(),\n (string) $response->getBody()\n );\n }\n );\n }\n\n /**\n * Create request for operation 'update'\n *\n * @param string $deal_id (required)\n * @param \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectInput $simple_public_object_input (required)\n * @param string $id_property The name of a property whose values are unique for this object type (optional)\n *\n * @throws \\InvalidArgumentException\n * @return \\GuzzleHttp\\Psr7\\Request\n */\n public function updateRequest($deal_id, $simple_public_object_input, $id_property = null)\n {\n // verify the required parameter 'deal_id' is set\n if ($deal_id === null || (is_array($deal_id) && count($deal_id) === 0)) {\n throw new \\InvalidArgumentException(\n 'Missing the required parameter $deal_id when calling update'\n );\n }\n // verify the required parameter 'simple_public_object_input' is set\n if ($simple_public_object_input === null || (is_array($simple_public_object_input) && count($simple_public_object_input) === 0)) {\n throw new \\InvalidArgumentException(\n 'Missing the required parameter $simple_public_object_input when calling update'\n );\n }\n\n $resourcePath = '/crm/v3/objects/deals/{dealId}';\n $formParams = [];\n $queryParams = [];\n $headerParams = [];\n $httpBody = '';\n $multipart = false;\n\n // query params\n if ($id_property !== null) {\n if('form' === 'form' && is_array($id_property)) {\n foreach($id_property as $key => $value) {\n $queryParams[$key] = $value;\n }\n }\n else {\n $queryParams['idProperty'] = $id_property;\n }\n }\n\n\n // path params\n if ($deal_id !== null) {\n $resourcePath = str_replace(\n '{' . 'dealId' . '}',\n ObjectSerializer::toPathValue($deal_id),\n $resourcePath\n );\n }\n\n\n if ($multipart) {\n $headers = $this->headerSelector->selectHeadersForMultipart(\n ['application/json', '*/*']\n );\n } else {\n $headers = $this->headerSelector->selectHeaders(\n ['application/json', '*/*'],\n ['application/json']\n );\n }\n\n // for model (json/xml)\n if (isset($simple_public_object_input)) {\n if ($headers['Content-Type'] === 'application/json') {\n $httpBody = \\GuzzleHttp\\json_encode(ObjectSerializer::sanitizeForSerialization($simple_public_object_input));\n } else {\n $httpBody = $simple_public_object_input;\n }\n } elseif (count($formParams) > 0) {\n if ($multipart) {\n $multipartContents = [];\n foreach ($formParams as $formParamName => $formParamValue) {\n $formParamValueItems = is_array($formParamValue) ? $formParamValue : [$formParamValue];\n foreach ($formParamValueItems as $formParamValueItem) {\n $multipartContents[] = [\n 'name' => $formParamName,\n 'contents' => $formParamValueItem\n ];\n }\n }\n // for HTTP post (form)\n $httpBody = new MultipartStream($multipartContents);\n\n } elseif ($headers['Content-Type'] === 'application/json') {\n $httpBody = \\GuzzleHttp\\json_encode($formParams);\n\n } else {\n // for HTTP post (form)\n $httpBody = \\GuzzleHttp\\Psr7\\Query::build($formParams);\n }\n }\n\n // this endpoint requires API key authentication\n $apiKey = $this->config->getApiKeyWithPrefix('hapikey');\n if ($apiKey !== null) {\n $queryParams['hapikey'] = $apiKey;\n }\n // this endpoint requires OAuth (access token)\n if ($this->config->getAccessToken() !== null) {\n $headers['Authorization'] = 'Bearer ' . $this->config->getAccessToken();\n }\n\n $defaultHeaders = [];\n if ($this->config->getUserAgent()) {\n $defaultHeaders['User-Agent'] = $this->config->getUserAgent();\n }\n\n $headers = array_merge(\n $defaultHeaders,\n $headerParams,\n $headers\n );\n\n $query = \\GuzzleHttp\\Psr7\\Query::build($queryParams);\n return new Request(\n 'PATCH',\n $this->config->getHost() . $resourcePath . ($query ? \"?{$query}\" : ''),\n $headers,\n $httpBody\n );\n }\n\n /**\n * Create http client option\n *\n * @throws \\RuntimeException on file opening failure\n * @return array of http client options\n */\n protected function createHttpClientOption()\n {\n $options = [];\n if ($this->config->getDebug()) {\n $options[RequestOptions::DEBUG] = fopen($this->config->getDebugFile(), 'a');\n if (!$options[RequestOptions::DEBUG]) {\n throw new \\RuntimeException('Failed to open the debug file: ' . $this->config->getDebugFile());\n }\n }\n\n return $options;\n }\n}","depth":4,"on_screen":true,"value":"<?php\n/**\n * BasicApi\n * PHP version 7.3\n *\n * @category Class\n * @package HubSpot\\Client\\Crm\\Deals\n * @author OpenAPI Generator team\n * @link https://openapi-generator.tech\n */\n\n/**\n * Deals\n *\n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: v3\n * Generated by: https://openapi-generator.tech\n * OpenAPI Generator version: 5.3.0\n */\n\n/**\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\nnamespace HubSpot\\Client\\Crm\\Deals\\Api;\n\nuse GuzzleHttp\\Client;\nuse GuzzleHttp\\ClientInterface;\nuse GuzzleHttp\\Exception\\RequestException;\nuse GuzzleHttp\\Exception\\ConnectException;\nuse GuzzleHttp\\Psr7\\MultipartStream;\nuse GuzzleHttp\\Psr7\\Request;\nuse GuzzleHttp\\RequestOptions;\nuse HubSpot\\Client\\Crm\\Deals\\ApiException;\nuse HubSpot\\Client\\Crm\\Deals\\Configuration;\nuse HubSpot\\Client\\Crm\\Deals\\HeaderSelector;\nuse HubSpot\\Client\\Crm\\Deals\\ObjectSerializer;\n\n/**\n * BasicApi Class Doc Comment\n *\n * @category Class\n * @package HubSpot\\Client\\Crm\\Deals\n * @author OpenAPI Generator team\n * @link https://openapi-generator.tech\n */\nclass BasicApi\n{\n /**\n * @var ClientInterface\n */\n protected $client;\n\n /**\n * @var Configuration\n */\n protected $config;\n\n /**\n * @var HeaderSelector\n */\n protected $headerSelector;\n\n /**\n * @var int Host index\n */\n protected $hostIndex;\n\n /**\n * @param ClientInterface $client\n * @param Configuration $config\n * @param HeaderSelector $selector\n * @param int $hostIndex (Optional) host index to select the list of hosts if defined in the OpenAPI spec\n */\n public function __construct(\n ClientInterface $client = null,\n Configuration $config = null,\n HeaderSelector $selector = null,\n $hostIndex = 0\n ) {\n $this->client = $client ?: new Client();\n $this->config = $config ?: new Configuration();\n $this->headerSelector = $selector ?: new HeaderSelector();\n $this->hostIndex = $hostIndex;\n }\n\n /**\n * Set the host index\n *\n * @param int $hostIndex Host index (required)\n */\n public function setHostIndex($hostIndex): void\n {\n $this->hostIndex = $hostIndex;\n }\n\n /**\n * Get the host index\n *\n * @return int Host index\n */\n public function getHostIndex()\n {\n return $this->hostIndex;\n }\n\n /**\n * @return Configuration\n */\n public function getConfig()\n {\n return $this->config;\n }\n\n /**\n * Operation archive\n *\n * Archive\n *\n * @param string $deal_id deal_id (required)\n *\n * @throws \\HubSpot\\Client\\Crm\\Deals\\ApiException on non-2xx response\n * @throws \\InvalidArgumentException\n * @return void\n */\n public function archive($deal_id)\n {\n $this->archiveWithHttpInfo($deal_id);\n }\n\n /**\n * Operation archiveWithHttpInfo\n *\n * Archive\n *\n * @param string $deal_id (required)\n *\n * @throws \\HubSpot\\Client\\Crm\\Deals\\ApiException on non-2xx response\n * @throws \\InvalidArgumentException\n * @return array of null, HTTP status code, HTTP response headers (array of strings)\n */\n public function archiveWithHttpInfo($deal_id)\n {\n $request = $this->archiveRequest($deal_id);\n\n try {\n $options = $this->createHttpClientOption();\n try {\n $response = $this->client->send($request, $options);\n } catch (RequestException $e) {\n throw new ApiException(\n \"[{$e->getCode()}] {$e->getMessage()}\",\n (int) $e->getCode(),\n $e->getResponse() ? $e->getResponse()->getHeaders() : null,\n $e->getResponse() ? (string) $e->getResponse()->getBody() : null\n );\n } catch (ConnectException $e) {\n throw new ApiException(\n \"[{$e->getCode()}] {$e->getMessage()}\",\n (int) $e->getCode(),\n null,\n null\n );\n }\n\n $statusCode = $response->getStatusCode();\n\n if ($statusCode < 200 || $statusCode > 299) {\n throw new ApiException(\n sprintf(\n '[%d] Error connecting to the API (%s)',\n $statusCode,\n (string) $request->getUri()\n ),\n $statusCode,\n $response->getHeaders(),\n (string) $response->getBody()\n );\n }\n\n return [null, $statusCode, $response->getHeaders()];\n\n } catch (ApiException $e) {\n switch ($e->getCode()) {\n default:\n $data = ObjectSerializer::deserialize(\n $e->getResponseBody(),\n '\\HubSpot\\Client\\Crm\\Deals\\Model\\Error',\n $e->getResponseHeaders()\n );\n $e->setResponseObject($data);\n break;\n }\n throw $e;\n }\n }\n\n /**\n * Operation archiveAsync\n *\n * Archive\n *\n * @param string $deal_id (required)\n *\n * @throws \\InvalidArgumentException\n * @return \\GuzzleHttp\\Promise\\PromiseInterface\n */\n public function archiveAsync($deal_id)\n {\n return $this->archiveAsyncWithHttpInfo($deal_id)\n ->then(\n function ($response) {\n return $response[0];\n }\n );\n }\n\n /**\n * Operation archiveAsyncWithHttpInfo\n *\n * Archive\n *\n * @param string $deal_id (required)\n *\n * @throws \\InvalidArgumentException\n * @return \\GuzzleHttp\\Promise\\PromiseInterface\n */\n public function archiveAsyncWithHttpInfo($deal_id)\n {\n $returnType = '';\n $request = $this->archiveRequest($deal_id);\n\n return $this->client\n ->sendAsync($request, $this->createHttpClientOption())\n ->then(\n function ($response) use ($returnType) {\n return [null, $response->getStatusCode(), $response->getHeaders()];\n },\n function ($exception) {\n $response = $exception->getResponse();\n $statusCode = $response->getStatusCode();\n throw new ApiException(\n sprintf(\n '[%d] Error connecting to the API (%s)',\n $statusCode,\n $exception->getRequest()->getUri()\n ),\n $statusCode,\n $response->getHeaders(),\n (string) $response->getBody()\n );\n }\n );\n }\n\n /**\n * Create request for operation 'archive'\n *\n * @param string $deal_id (required)\n *\n * @throws \\InvalidArgumentException\n * @return \\GuzzleHttp\\Psr7\\Request\n */\n public function archiveRequest($deal_id)\n {\n // verify the required parameter 'deal_id' is set\n if ($deal_id === null || (is_array($deal_id) && count($deal_id) === 0)) {\n throw new \\InvalidArgumentException(\n 'Missing the required parameter $deal_id when calling archive'\n );\n }\n\n $resourcePath = '/crm/v3/objects/deals/{dealId}';\n $formParams = [];\n $queryParams = [];\n $headerParams = [];\n $httpBody = '';\n $multipart = false;\n\n\n\n // path params\n if ($deal_id !== null) {\n $resourcePath = str_replace(\n '{' . 'dealId' . '}',\n ObjectSerializer::toPathValue($deal_id),\n $resourcePath\n );\n }\n\n\n if ($multipart) {\n $headers = $this->headerSelector->selectHeadersForMultipart(\n ['*/*']\n );\n } else {\n $headers = $this->headerSelector->selectHeaders(\n ['*/*'],\n []\n );\n }\n\n // for model (json/xml)\n if (count($formParams) > 0) {\n if ($multipart) {\n $multipartContents = [];\n foreach ($formParams as $formParamName => $formParamValue) {\n $formParamValueItems = is_array($formParamValue) ? $formParamValue : [$formParamValue];\n foreach ($formParamValueItems as $formParamValueItem) {\n $multipartContents[] = [\n 'name' => $formParamName,\n 'contents' => $formParamValueItem\n ];\n }\n }\n // for HTTP post (form)\n $httpBody = new MultipartStream($multipartContents);\n\n } elseif ($headers['Content-Type'] === 'application/json') {\n $httpBody = \\GuzzleHttp\\json_encode($formParams);\n\n } else {\n // for HTTP post (form)\n $httpBody = \\GuzzleHttp\\Psr7\\Query::build($formParams);\n }\n }\n\n // this endpoint requires API key authentication\n $apiKey = $this->config->getApiKeyWithPrefix('hapikey');\n if ($apiKey !== null) {\n $queryParams['hapikey'] = $apiKey;\n }\n // this endpoint requires OAuth (access token)\n if ($this->config->getAccessToken() !== null) {\n $headers['Authorization'] = 'Bearer ' . $this->config->getAccessToken();\n }\n\n $defaultHeaders = [];\n if ($this->config->getUserAgent()) {\n $defaultHeaders['User-Agent'] = $this->config->getUserAgent();\n }\n\n $headers = array_merge(\n $defaultHeaders,\n $headerParams,\n $headers\n );\n\n $query = \\GuzzleHttp\\Psr7\\Query::build($queryParams);\n return new Request(\n 'DELETE',\n $this->config->getHost() . $resourcePath . ($query ? \"?{$query}\" : ''),\n $headers,\n $httpBody\n );\n }\n\n /**\n * Operation create\n *\n * Create\n *\n * @param \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectInput $simple_public_object_input simple_public_object_input (required)\n *\n * @throws \\HubSpot\\Client\\Crm\\Deals\\ApiException on non-2xx response\n * @throws \\InvalidArgumentException\n * @return \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObject|\\HubSpot\\Client\\Crm\\Deals\\Model\\Error\n */\n public function create($simple_public_object_input)\n {\n list($response) = $this->createWithHttpInfo($simple_public_object_input);\n return $response;\n }\n\n /**\n * Operation createWithHttpInfo\n *\n * Create\n *\n * @param \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectInput $simple_public_object_input (required)\n *\n * @throws \\HubSpot\\Client\\Crm\\Deals\\ApiException on non-2xx response\n * @throws \\InvalidArgumentException\n * @return array of \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObject|\\HubSpot\\Client\\Crm\\Deals\\Model\\Error, HTTP status code, HTTP response headers (array of strings)\n */\n public function createWithHttpInfo($simple_public_object_input)\n {\n $request = $this->createRequest($simple_public_object_input);\n\n try {\n $options = $this->createHttpClientOption();\n try {\n $response = $this->client->send($request, $options);\n } catch (RequestException $e) {\n throw new ApiException(\n \"[{$e->getCode()}] {$e->getMessage()}\",\n (int) $e->getCode(),\n $e->getResponse() ? $e->getResponse()->getHeaders() : null,\n $e->getResponse() ? (string) $e->getResponse()->getBody() : null\n );\n } catch (ConnectException $e) {\n throw new ApiException(\n \"[{$e->getCode()}] {$e->getMessage()}\",\n (int) $e->getCode(),\n null,\n null\n );\n }\n\n $statusCode = $response->getStatusCode();\n\n if ($statusCode < 200 || $statusCode > 299) {\n throw new ApiException(\n sprintf(\n '[%d] Error connecting to the API (%s)',\n $statusCode,\n (string) $request->getUri()\n ),\n $statusCode,\n $response->getHeaders(),\n (string) $response->getBody()\n );\n }\n\n switch($statusCode) {\n case 201:\n if ('\\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObject' === '\\SplFileObject') {\n $content = $response->getBody(); //stream goes to serializer\n } else {\n $content = (string) $response->getBody();\n }\n\n return [\n ObjectSerializer::deserialize($content, '\\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObject', []),\n $response->getStatusCode(),\n $response->getHeaders()\n ];\n default:\n if ('\\HubSpot\\Client\\Crm\\Deals\\Model\\Error' === '\\SplFileObject') {\n $content = $response->getBody(); //stream goes to serializer\n } else {\n $content = (string) $response->getBody();\n }\n\n return [\n ObjectSerializer::deserialize($content, '\\HubSpot\\Client\\Crm\\Deals\\Model\\Error', []),\n $response->getStatusCode(),\n $response->getHeaders()\n ];\n }\n\n $returnType = '\\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObject';\n if ($returnType === '\\SplFileObject') {\n $content = $response->getBody(); //stream goes to serializer\n } else {\n $content = (string) $response->getBody();\n }\n\n return [\n ObjectSerializer::deserialize($content, $returnType, []),\n $response->getStatusCode(),\n $response->getHeaders()\n ];\n\n } catch (ApiException $e) {\n switch ($e->getCode()) {\n case 201:\n $data = ObjectSerializer::deserialize(\n $e->getResponseBody(),\n '\\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObject',\n $e->getResponseHeaders()\n );\n $e->setResponseObject($data);\n break;\n default:\n $data = ObjectSerializer::deserialize(\n $e->getResponseBody(),\n '\\HubSpot\\Client\\Crm\\Deals\\Model\\Error',\n $e->getResponseHeaders()\n );\n $e->setResponseObject($data);\n break;\n }\n throw $e;\n }\n }\n\n /**\n * Operation createAsync\n *\n * Create\n *\n * @param \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectInput $simple_public_object_input (required)\n *\n * @throws \\InvalidArgumentException\n * @return \\GuzzleHttp\\Promise\\PromiseInterface\n */\n public function createAsync($simple_public_object_input)\n {\n return $this->createAsyncWithHttpInfo($simple_public_object_input)\n ->then(\n function ($response) {\n return $response[0];\n }\n );\n }\n\n /**\n * Operation createAsyncWithHttpInfo\n *\n * Create\n *\n * @param \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectInput $simple_public_object_input (required)\n *\n * @throws \\InvalidArgumentException\n * @return \\GuzzleHttp\\Promise\\PromiseInterface\n */\n public function createAsyncWithHttpInfo($simple_public_object_input)\n {\n $returnType = '\\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObject';\n $request = $this->createRequest($simple_public_object_input);\n\n return $this->client\n ->sendAsync($request, $this->createHttpClientOption())\n ->then(\n function ($response) use ($returnType) {\n if ($returnType === '\\SplFileObject') {\n $content = $response->getBody(); //stream goes to serializer\n } else {\n $content = (string) $response->getBody();\n }\n\n return [\n ObjectSerializer::deserialize($content, $returnType, []),\n $response->getStatusCode(),\n $response->getHeaders()\n ];\n },\n function ($exception) {\n $response = $exception->getResponse();\n $statusCode = $response->getStatusCode();\n throw new ApiException(\n sprintf(\n '[%d] Error connecting to the API (%s)',\n $statusCode,\n $exception->getRequest()->getUri()\n ),\n $statusCode,\n $response->getHeaders(),\n (string) $response->getBody()\n );\n }\n );\n }\n\n /**\n * Create request for operation 'create'\n *\n * @param \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectInput $simple_public_object_input (required)\n *\n * @throws \\InvalidArgumentException\n * @return \\GuzzleHttp\\Psr7\\Request\n */\n public function createRequest($simple_public_object_input)\n {\n // verify the required parameter 'simple_public_object_input' is set\n if ($simple_public_object_input === null || (is_array($simple_public_object_input) && count($simple_public_object_input) === 0)) {\n throw new \\InvalidArgumentException(\n 'Missing the required parameter $simple_public_object_input when calling create'\n );\n }\n\n $resourcePath = '/crm/v3/objects/deals';\n $formParams = [];\n $queryParams = [];\n $headerParams = [];\n $httpBody = '';\n $multipart = false;\n\n\n\n\n\n if ($multipart) {\n $headers = $this->headerSelector->selectHeadersForMultipart(\n ['application/json', '*/*']\n );\n } else {\n $headers = $this->headerSelector->selectHeaders(\n ['application/json', '*/*'],\n ['application/json']\n );\n }\n\n // for model (json/xml)\n if (isset($simple_public_object_input)) {\n if ($headers['Content-Type'] === 'application/json') {\n $httpBody = \\GuzzleHttp\\json_encode(ObjectSerializer::sanitizeForSerialization($simple_public_object_input));\n } else {\n $httpBody = $simple_public_object_input;\n }\n } elseif (count($formParams) > 0) {\n if ($multipart) {\n $multipartContents = [];\n foreach ($formParams as $formParamName => $formParamValue) {\n $formParamValueItems = is_array($formParamValue) ? $formParamValue : [$formParamValue];\n foreach ($formParamValueItems as $formParamValueItem) {\n $multipartContents[] = [\n 'name' => $formParamName,\n 'contents' => $formParamValueItem\n ];\n }\n }\n // for HTTP post (form)\n $httpBody = new MultipartStream($multipartContents);\n\n } elseif ($headers['Content-Type'] === 'application/json') {\n $httpBody = \\GuzzleHttp\\json_encode($formParams);\n\n } else {\n // for HTTP post (form)\n $httpBody = \\GuzzleHttp\\Psr7\\Query::build($formParams);\n }\n }\n\n // this endpoint requires API key authentication\n $apiKey = $this->config->getApiKeyWithPrefix('hapikey');\n if ($apiKey !== null) {\n $queryParams['hapikey'] = $apiKey;\n }\n // this endpoint requires OAuth (access token)\n if ($this->config->getAccessToken() !== null) {\n $headers['Authorization'] = 'Bearer ' . $this->config->getAccessToken();\n }\n\n $defaultHeaders = [];\n if ($this->config->getUserAgent()) {\n $defaultHeaders['User-Agent'] = $this->config->getUserAgent();\n }\n\n $headers = array_merge(\n $defaultHeaders,\n $headerParams,\n $headers\n );\n\n $query = \\GuzzleHttp\\Psr7\\Query::build($queryParams);\n return new Request(\n 'POST',\n $this->config->getHost() . $resourcePath . ($query ? \"?{$query}\" : ''),\n $headers,\n $httpBody\n );\n }\n\n /**\n * Operation getById\n *\n * Read\n *\n * @param string $deal_id deal_id (required)\n * @param string[] $properties A comma separated list of the properties to be returned in the response. If any of the specified properties are not present on the requested object(s), they will be ignored. (optional)\n * @param string[] $associations A comma separated list of object types to retrieve associated IDs for. If any of the specified associations do not exist, they will be ignored. (optional)\n * @param bool $archived Whether to return only results that have been archived. (optional, default to false)\n * @param string $id_property The name of a property whose values are unique for this object type (optional)\n *\n * @throws \\HubSpot\\Client\\Crm\\Deals\\ApiException on non-2xx response\n * @throws \\InvalidArgumentException\n * @return \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations|\\HubSpot\\Client\\Crm\\Deals\\Model\\Error\n */\n public function getById($deal_id, $properties = null, $associations = null, $archived = false, $id_property = null)\n {\n list($response) = $this->getByIdWithHttpInfo($deal_id, $properties, $associations, $archived, $id_property);\n return $response;\n }\n\n /**\n * Operation getByIdWithHttpInfo\n *\n * Read\n *\n * @param string $deal_id (required)\n * @param string[] $properties A comma separated list of the properties to be returned in the response. If any of the specified properties are not present on the requested object(s), they will be ignored. (optional)\n * @param string[] $associations A comma separated list of object types to retrieve associated IDs for. If any of the specified associations do not exist, they will be ignored. (optional)\n * @param bool $archived Whether to return only results that have been archived. (optional, default to false)\n * @param string $id_property The name of a property whose values are unique for this object type (optional)\n *\n * @throws \\HubSpot\\Client\\Crm\\Deals\\ApiException on non-2xx response\n * @throws \\InvalidArgumentException\n * @return array of \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations|\\HubSpot\\Client\\Crm\\Deals\\Model\\Error, HTTP status code, HTTP response headers (array of strings)\n */\n public function getByIdWithHttpInfo($deal_id, $properties = null, $associations = null, $archived = false, $id_property = null)\n {\n $request = $this->getByIdRequest($deal_id, $properties, $associations, $archived, $id_property);\n\n try {\n $options = $this->createHttpClientOption();\n try {\n $response = $this->client->send($request, $options);\n } catch (RequestException $e) {\n throw new ApiException(\n \"[{$e->getCode()}] {$e->getMessage()}\",\n (int) $e->getCode(),\n $e->getResponse() ? $e->getResponse()->getHeaders() : null,\n $e->getResponse() ? (string) $e->getResponse()->getBody() : null\n );\n } catch (ConnectException $e) {\n throw new ApiException(\n \"[{$e->getCode()}] {$e->getMessage()}\",\n (int) $e->getCode(),\n null,\n null\n );\n }\n\n $statusCode = $response->getStatusCode();\n\n if ($statusCode < 200 || $statusCode > 299) {\n throw new ApiException(\n sprintf(\n '[%d] Error connecting to the API (%s)',\n $statusCode,\n (string) $request->getUri()\n ),\n $statusCode,\n $response->getHeaders(),\n (string) $response->getBody()\n );\n }\n\n switch($statusCode) {\n case 200:\n if ('\\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations' === '\\SplFileObject') {\n $content = $response->getBody(); //stream goes to serializer\n } else {\n $content = (string) $response->getBody();\n }\n\n return [\n ObjectSerializer::deserialize($content, '\\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations', []),\n $response->getStatusCode(),\n $response->getHeaders()\n ];\n default:\n if ('\\HubSpot\\Client\\Crm\\Deals\\Model\\Error' === '\\SplFileObject') {\n $content = $response->getBody(); //stream goes to serializer\n } else {\n $content = (string) $response->getBody();\n }\n\n return [\n ObjectSerializer::deserialize($content, '\\HubSpot\\Client\\Crm\\Deals\\Model\\Error', []),\n $response->getStatusCode(),\n $response->getHeaders()\n ];\n }\n\n $returnType = '\\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations';\n if ($returnType === '\\SplFileObject') {\n $content = $response->getBody(); //stream goes to serializer\n } else {\n $content = (string) $response->getBody();\n }\n\n return [\n ObjectSerializer::deserialize($content, $returnType, []),\n $response->getStatusCode(),\n $response->getHeaders()\n ];\n\n } catch (ApiException $e) {\n switch ($e->getCode()) {\n case 200:\n $data = ObjectSerializer::deserialize(\n $e->getResponseBody(),\n '\\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations',\n $e->getResponseHeaders()\n );\n $e->setResponseObject($data);\n break;\n default:\n $data = ObjectSerializer::deserialize(\n $e->getResponseBody(),\n '\\HubSpot\\Client\\Crm\\Deals\\Model\\Error',\n $e->getResponseHeaders()\n );\n $e->setResponseObject($data);\n break;\n }\n throw $e;\n }\n }\n\n /**\n * Operation getByIdAsync\n *\n * Read\n *\n * @param string $deal_id (required)\n * @param string[] $properties A comma separated list of the properties to be returned in the response. If any of the specified properties are not present on the requested object(s), they will be ignored. (optional)\n * @param string[] $associations A comma separated list of object types to retrieve associated IDs for. If any of the specified associations do not exist, they will be ignored. (optional)\n * @param bool $archived Whether to return only results that have been archived. (optional, default to false)\n * @param string $id_property The name of a property whose values are unique for this object type (optional)\n *\n * @throws \\InvalidArgumentException\n * @return \\GuzzleHttp\\Promise\\PromiseInterface\n */\n public function getByIdAsync($deal_id, $properties = null, $associations = null, $archived = false, $id_property = null)\n {\n return $this->getByIdAsyncWithHttpInfo($deal_id, $properties, $associations, $archived, $id_property)\n ->then(\n function ($response) {\n return $response[0];\n }\n );\n }\n\n /**\n * Operation getByIdAsyncWithHttpInfo\n *\n * Read\n *\n * @param string $deal_id (required)\n * @param string[] $properties A comma separated list of the properties to be returned in the response. If any of the specified properties are not present on the requested object(s), they will be ignored. (optional)\n * @param string[] $associations A comma separated list of object types to retrieve associated IDs for. If any of the specified associations do not exist, they will be ignored. (optional)\n * @param bool $archived Whether to return only results that have been archived. (optional, default to false)\n * @param string $id_property The name of a property whose values are unique for this object type (optional)\n *\n * @throws \\InvalidArgumentException\n * @return \\GuzzleHttp\\Promise\\PromiseInterface\n */\n public function getByIdAsyncWithHttpInfo($deal_id, $properties = null, $associations = null, $archived = false, $id_property = null)\n {\n $returnType = '\\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations';\n $request = $this->getByIdRequest($deal_id, $properties, $associations, $archived, $id_property);\n\n return $this->client\n ->sendAsync($request, $this->createHttpClientOption())\n ->then(\n function ($response) use ($returnType) {\n if ($returnType === '\\SplFileObject') {\n $content = $response->getBody(); //stream goes to serializer\n } else {\n $content = (string) $response->getBody();\n }\n\n return [\n ObjectSerializer::deserialize($content, $returnType, []),\n $response->getStatusCode(),\n $response->getHeaders()\n ];\n },\n function ($exception) {\n $response = $exception->getResponse();\n $statusCode = $response->getStatusCode();\n throw new ApiException(\n sprintf(\n '[%d] Error connecting to the API (%s)',\n $statusCode,\n $exception->getRequest()->getUri()\n ),\n $statusCode,\n $response->getHeaders(),\n (string) $response->getBody()\n );\n }\n );\n }\n\n /**\n * Create request for operation 'getById'\n *\n * @param string $deal_id (required)\n * @param string[] $properties A comma separated list of the properties to be returned in the response. If any of the specified properties are not present on the requested object(s), they will be ignored. (optional)\n * @param string[] $associations A comma separated list of object types to retrieve associated IDs for. If any of the specified associations do not exist, they will be ignored. (optional)\n * @param bool $archived Whether to return only results that have been archived. (optional, default to false)\n * @param string $id_property The name of a property whose values are unique for this object type (optional)\n *\n * @throws \\InvalidArgumentException\n * @return \\GuzzleHttp\\Psr7\\Request\n */\n public function getByIdRequest($deal_id, $properties = null, $associations = null, $archived = false, $id_property = null)\n {\n // verify the required parameter 'deal_id' is set\n if ($deal_id === null || (is_array($deal_id) && count($deal_id) === 0)) {\n throw new \\InvalidArgumentException(\n 'Missing the required parameter $deal_id when calling getById'\n );\n }\n\n $resourcePath = '/crm/v3/objects/deals/{dealId}';\n $formParams = [];\n $queryParams = [];\n $headerParams = [];\n $httpBody = '';\n $multipart = false;\n\n // query params\n if ($properties !== null) {\n if('form' === 'form' && is_array($properties)) {\n foreach($properties as $key => $value) {\n $queryParams[$key] = $value;\n }\n }\n else {\n $queryParams['properties'] = $properties;\n }\n }\n // query params\n if ($associations !== null) {\n if('form' === 'form' && is_array($associations)) {\n foreach($associations as $key => $value) {\n $queryParams[$key] = $value;\n }\n }\n else {\n $queryParams['associations'] = $associations;\n }\n }\n // query params\n if ($archived !== null) {\n if('form' === 'form' && is_array($archived)) {\n foreach($archived as $key => $value) {\n $queryParams[$key] = $value;\n }\n }\n else {\n $queryParams['archived'] = $archived;\n }\n }\n // query params\n if ($id_property !== null) {\n if('form' === 'form' && is_array($id_property)) {\n foreach($id_property as $key => $value) {\n $queryParams[$key] = $value;\n }\n }\n else {\n $queryParams['idProperty'] = $id_property;\n }\n }\n\n\n // path params\n if ($deal_id !== null) {\n $resourcePath = str_replace(\n '{' . 'dealId' . '}',\n ObjectSerializer::toPathValue($deal_id),\n $resourcePath\n );\n }\n\n\n if ($multipart) {\n $headers = $this->headerSelector->selectHeadersForMultipart(\n ['application/json', '*/*']\n );\n } else {\n $headers = $this->headerSelector->selectHeaders(\n ['application/json', '*/*'],\n []\n );\n }\n\n // for model (json/xml)\n if (count($formParams) > 0) {\n if ($multipart) {\n $multipartContents = [];\n foreach ($formParams as $formParamName => $formParamValue) {\n $formParamValueItems = is_array($formParamValue) ? $formParamValue : [$formParamValue];\n foreach ($formParamValueItems as $formParamValueItem) {\n $multipartContents[] = [\n 'name' => $formParamName,\n 'contents' => $formParamValueItem\n ];\n }\n }\n // for HTTP post (form)\n $httpBody = new MultipartStream($multipartContents);\n\n } elseif ($headers['Content-Type'] === 'application/json') {\n $httpBody = \\GuzzleHttp\\json_encode($formParams);\n\n } else {\n // for HTTP post (form)\n $httpBody = \\GuzzleHttp\\Psr7\\Query::build($formParams);\n }\n }\n\n // this endpoint requires API key authentication\n $apiKey = $this->config->getApiKeyWithPrefix('hapikey');\n if ($apiKey !== null) {\n $queryParams['hapikey'] = $apiKey;\n }\n // this endpoint requires OAuth (access token)\n if ($this->config->getAccessToken() !== null) {\n $headers['Authorization'] = 'Bearer ' . $this->config->getAccessToken();\n }\n\n $defaultHeaders = [];\n if ($this->config->getUserAgent()) {\n $defaultHeaders['User-Agent'] = $this->config->getUserAgent();\n }\n\n $headers = array_merge(\n $defaultHeaders,\n $headerParams,\n $headers\n );\n\n $query = \\GuzzleHttp\\Psr7\\Query::build($queryParams);\n return new Request(\n 'GET',\n $this->config->getHost() . $resourcePath . ($query ? \"?{$query}\" : ''),\n $headers,\n $httpBody\n );\n }\n\n /**\n * Operation getPage\n *\n * List\n *\n * @param int $limit The maximum number of results to display per page. (optional, default to 10)\n * @param string $after The paging cursor token of the last successfully read resource will be returned as the `paging.next.after` JSON property of a paged response containing more results. (optional)\n * @param string[] $properties A comma separated list of the properties to be returned in the response. If any of the specified properties are not present on the requested object(s), they will be ignored. (optional)\n * @param string[] $associations A comma separated list of object types to retrieve associated IDs for. If any of the specified associations do not exist, they will be ignored. (optional)\n * @param bool $archived Whether to return only results that have been archived. (optional, default to false)\n *\n * @throws \\HubSpot\\Client\\Crm\\Deals\\ApiException on non-2xx response\n * @throws \\InvalidArgumentException\n * @return \\HubSpot\\Client\\Crm\\Deals\\Model\\CollectionResponseSimplePublicObjectWithAssociationsForwardPaging|\\HubSpot\\Client\\Crm\\Deals\\Model\\Error\n */\n public function getPage($limit = 10, $after = null, $properties = null, $associations = null, $archived = false)\n {\n list($response) = $this->getPageWithHttpInfo($limit, $after, $properties, $associations, $archived);\n return $response;\n }\n\n /**\n * Operation getPageWithHttpInfo\n *\n * List\n *\n * @param int $limit The maximum number of results to display per page. (optional, default to 10)\n * @param string $after The paging cursor token of the last successfully read resource will be returned as the `paging.next.after` JSON property of a paged response containing more results. (optional)\n * @param string[] $properties A comma separated list of the properties to be returned in the response. If any of the specified properties are not present on the requested object(s), they will be ignored. (optional)\n * @param string[] $associations A comma separated list of object types to retrieve associated IDs for. If any of the specified associations do not exist, they will be ignored. (optional)\n * @param bool $archived Whether to return only results that have been archived. (optional, default to false)\n *\n * @throws \\HubSpot\\Client\\Crm\\Deals\\ApiException on non-2xx response\n * @throws \\InvalidArgumentException\n * @return array of \\HubSpot\\Client\\Crm\\Deals\\Model\\CollectionResponseSimplePublicObjectWithAssociationsForwardPaging|\\HubSpot\\Client\\Crm\\Deals\\Model\\Error, HTTP status code, HTTP response headers (array of strings)\n */\n public function getPageWithHttpInfo($limit = 10, $after = null, $properties = null, $associations = null, $archived = false)\n {\n $request = $this->getPageRequest($limit, $after, $properties, $associations, $archived);\n\n try {\n $options = $this->createHttpClientOption();\n try {\n $response = $this->client->send($request, $options);\n } catch (RequestException $e) {\n throw new ApiException(\n \"[{$e->getCode()}] {$e->getMessage()}\",\n (int) $e->getCode(),\n $e->getResponse() ? $e->getResponse()->getHeaders() : null,\n $e->getResponse() ? (string) $e->getResponse()->getBody() : null\n );\n } catch (ConnectException $e) {\n throw new ApiException(\n \"[{$e->getCode()}] {$e->getMessage()}\",\n (int) $e->getCode(),\n null,\n null\n );\n }\n\n $statusCode = $response->getStatusCode();\n\n if ($statusCode < 200 || $statusCode > 299) {\n throw new ApiException(\n sprintf(\n '[%d] Error connecting to the API (%s)',\n $statusCode,\n (string) $request->getUri()\n ),\n $statusCode,\n $response->getHeaders(),\n (string) $response->getBody()\n );\n }\n\n switch($statusCode) {\n case 200:\n if ('\\HubSpot\\Client\\Crm\\Deals\\Model\\CollectionResponseSimplePublicObjectWithAssociationsForwardPaging' === '\\SplFileObject') {\n $content = $response->getBody(); //stream goes to serializer\n } else {\n $content = (string) $response->getBody();\n }\n\n return [\n ObjectSerializer::deserialize($content, '\\HubSpot\\Client\\Crm\\Deals\\Model\\CollectionResponseSimplePublicObjectWithAssociationsForwardPaging', []),\n $response->getStatusCode(),\n $response->getHeaders()\n ];\n default:\n if ('\\HubSpot\\Client\\Crm\\Deals\\Model\\Error' === '\\SplFileObject') {\n $content = $response->getBody(); //stream goes to serializer\n } else {\n $content = (string) $response->getBody();\n }\n\n return [\n ObjectSerializer::deserialize($content, '\\HubSpot\\Client\\Crm\\Deals\\Model\\Error', []),\n $response->getStatusCode(),\n $response->getHeaders()\n ];\n }\n\n $returnType = '\\HubSpot\\Client\\Crm\\Deals\\Model\\CollectionResponseSimplePublicObjectWithAssociationsForwardPaging';\n if ($returnType === '\\SplFileObject') {\n $content = $response->getBody(); //stream goes to serializer\n } else {\n $content = (string) $response->getBody();\n }\n\n return [\n ObjectSerializer::deserialize($content, $returnType, []),\n $response->getStatusCode(),\n $response->getHeaders()\n ];\n\n } catch (ApiException $e) {\n switch ($e->getCode()) {\n case 200:\n $data = ObjectSerializer::deserialize(\n $e->getResponseBody(),\n '\\HubSpot\\Client\\Crm\\Deals\\Model\\CollectionResponseSimplePublicObjectWithAssociationsForwardPaging',\n $e->getResponseHeaders()\n );\n $e->setResponseObject($data);\n break;\n default:\n $data = ObjectSerializer::deserialize(\n $e->getResponseBody(),\n '\\HubSpot\\Client\\Crm\\Deals\\Model\\Error',\n $e->getResponseHeaders()\n );\n $e->setResponseObject($data);\n break;\n }\n throw $e;\n }\n }\n\n /**\n * Operation getPageAsync\n *\n * List\n *\n * @param int $limit The maximum number of results to display per page. (optional, default to 10)\n * @param string $after The paging cursor token of the last successfully read resource will be returned as the `paging.next.after` JSON property of a paged response containing more results. (optional)\n * @param string[] $properties A comma separated list of the properties to be returned in the response. If any of the specified properties are not present on the requested object(s), they will be ignored. (optional)\n * @param string[] $associations A comma separated list of object types to retrieve associated IDs for. If any of the specified associations do not exist, they will be ignored. (optional)\n * @param bool $archived Whether to return only results that have been archived. (optional, default to false)\n *\n * @throws \\InvalidArgumentException\n * @return \\GuzzleHttp\\Promise\\PromiseInterface\n */\n public function getPageAsync($limit = 10, $after = null, $properties = null, $associations = null, $archived = false)\n {\n return $this->getPageAsyncWithHttpInfo($limit, $after, $properties, $associations, $archived)\n ->then(\n function ($response) {\n return $response[0];\n }\n );\n }\n\n /**\n * Operation getPageAsyncWithHttpInfo\n *\n * List\n *\n * @param int $limit The maximum number of results to display per page. (optional, default to 10)\n * @param string $after The paging cursor token of the last successfully read resource will be returned as the `paging.next.after` JSON property of a paged response containing more results. (optional)\n * @param string[] $properties A comma separated list of the properties to be returned in the response. If any of the specified properties are not present on the requested object(s), they will be ignored. (optional)\n * @param string[] $associations A comma separated list of object types to retrieve associated IDs for. If any of the specified associations do not exist, they will be ignored. (optional)\n * @param bool $archived Whether to return only results that have been archived. (optional, default to false)\n *\n * @throws \\InvalidArgumentException\n * @return \\GuzzleHttp\\Promise\\PromiseInterface\n */\n public function getPageAsyncWithHttpInfo($limit = 10, $after = null, $properties = null, $associations = null, $archived = false)\n {\n $returnType = '\\HubSpot\\Client\\Crm\\Deals\\Model\\CollectionResponseSimplePublicObjectWithAssociationsForwardPaging';\n $request = $this->getPageRequest($limit, $after, $properties, $associations, $archived);\n\n return $this->client\n ->sendAsync($request, $this->createHttpClientOption())\n ->then(\n function ($response) use ($returnType) {\n if ($returnType === '\\SplFileObject') {\n $content = $response->getBody(); //stream goes to serializer\n } else {\n $content = (string) $response->getBody();\n }\n\n return [\n ObjectSerializer::deserialize($content, $returnType, []),\n $response->getStatusCode(),\n $response->getHeaders()\n ];\n },\n function ($exception) {\n $response = $exception->getResponse();\n $statusCode = $response->getStatusCode();\n throw new ApiException(\n sprintf(\n '[%d] Error connecting to the API (%s)',\n $statusCode,\n $exception->getRequest()->getUri()\n ),\n $statusCode,\n $response->getHeaders(),\n (string) $response->getBody()\n );\n }\n );\n }\n\n /**\n * Create request for operation 'getPage'\n *\n * @param int $limit The maximum number of results to display per page. (optional, default to 10)\n * @param string $after The paging cursor token of the last successfully read resource will be returned as the `paging.next.after` JSON property of a paged response containing more results. (optional)\n * @param string[] $properties A comma separated list of the properties to be returned in the response. If any of the specified properties are not present on the requested object(s), they will be ignored. (optional)\n * @param string[] $associations A comma separated list of object types to retrieve associated IDs for. If any of the specified associations do not exist, they will be ignored. (optional)\n * @param bool $archived Whether to return only results that have been archived. (optional, default to false)\n *\n * @throws \\InvalidArgumentException\n * @return \\GuzzleHttp\\Psr7\\Request\n */\n public function getPageRequest($limit = 10, $after = null, $properties = null, $associations = null, $archived = false)\n {\n\n $resourcePath = '/crm/v3/objects/deals';\n $formParams = [];\n $queryParams = [];\n $headerParams = [];\n $httpBody = '';\n $multipart = false;\n\n // query params\n if ($limit !== null) {\n if('form' === 'form' && is_array($limit)) {\n foreach($limit as $key => $value) {\n $queryParams[$key] = $value;\n }\n }\n else {\n $queryParams['limit'] = $limit;\n }\n }\n // query params\n if ($after !== null) {\n if('form' === 'form' && is_array($after)) {\n foreach($after as $key => $value) {\n $queryParams[$key] = $value;\n }\n }\n else {\n $queryParams['after'] = $after;\n }\n }\n // query params\n if ($properties !== null) {\n if('form' === 'form' && is_array($properties)) {\n foreach($properties as $key => $value) {\n $queryParams[$key] = $value;\n }\n }\n else {\n $queryParams['properties'] = $properties;\n }\n }\n // query params\n if ($associations !== null) {\n if('form' === 'form' && is_array($associations)) {\n foreach($associations as $key => $value) {\n $queryParams[$key] = $value;\n }\n }\n else {\n $queryParams['associations'] = $associations;\n }\n }\n // query params\n if ($archived !== null) {\n if('form' === 'form' && is_array($archived)) {\n foreach($archived as $key => $value) {\n $queryParams[$key] = $value;\n }\n }\n else {\n $queryParams['archived'] = $archived;\n }\n }\n\n\n\n\n if ($multipart) {\n $headers = $this->headerSelector->selectHeadersForMultipart(\n ['application/json', '*/*']\n );\n } else {\n $headers = $this->headerSelector->selectHeaders(\n ['application/json', '*/*'],\n []\n );\n }\n\n // for model (json/xml)\n if (count($formParams) > 0) {\n if ($multipart) {\n $multipartContents = [];\n foreach ($formParams as $formParamName => $formParamValue) {\n $formParamValueItems = is_array($formParamValue) ? $formParamValue : [$formParamValue];\n foreach ($formParamValueItems as $formParamValueItem) {\n $multipartContents[] = [\n 'name' => $formParamName,\n 'contents' => $formParamValueItem\n ];\n }\n }\n // for HTTP post (form)\n $httpBody = new MultipartStream($multipartContents);\n\n } elseif ($headers['Content-Type'] === 'application/json') {\n $httpBody = \\GuzzleHttp\\json_encode($formParams);\n\n } else {\n // for HTTP post (form)\n $httpBody = \\GuzzleHttp\\Psr7\\Query::build($formParams);\n }\n }\n\n // this endpoint requires API key authentication\n $apiKey = $this->config->getApiKeyWithPrefix('hapikey');\n if ($apiKey !== null) {\n $queryParams['hapikey'] = $apiKey;\n }\n // this endpoint requires OAuth (access token)\n if ($this->config->getAccessToken() !== null) {\n $headers['Authorization'] = 'Bearer ' . $this->config->getAccessToken();\n }\n\n $defaultHeaders = [];\n if ($this->config->getUserAgent()) {\n $defaultHeaders['User-Agent'] = $this->config->getUserAgent();\n }\n\n $headers = array_merge(\n $defaultHeaders,\n $headerParams,\n $headers\n );\n\n $query = \\GuzzleHttp\\Psr7\\Query::build($queryParams);\n return new Request(\n 'GET',\n $this->config->getHost() . $resourcePath . ($query ? \"?{$query}\" : ''),\n $headers,\n $httpBody\n );\n }\n\n /**\n * Operation update\n *\n * Update\n *\n * @param string $deal_id deal_id (required)\n * @param \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectInput $simple_public_object_input simple_public_object_input (required)\n * @param string $id_property The name of a property whose values are unique for this object type (optional)\n *\n * @throws \\HubSpot\\Client\\Crm\\Deals\\ApiException on non-2xx response\n * @throws \\InvalidArgumentException\n * @return \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObject|\\HubSpot\\Client\\Crm\\Deals\\Model\\Error\n */\n public function update($deal_id, $simple_public_object_input, $id_property = null)\n {\n list($response) = $this->updateWithHttpInfo($deal_id, $simple_public_object_input, $id_property);\n return $response;\n }\n\n /**\n * Operation updateWithHttpInfo\n *\n * Update\n *\n * @param string $deal_id (required)\n * @param \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectInput $simple_public_object_input (required)\n * @param string $id_property The name of a property whose values are unique for this object type (optional)\n *\n * @throws \\HubSpot\\Client\\Crm\\Deals\\ApiException on non-2xx response\n * @throws \\InvalidArgumentException\n * @return array of \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObject|\\HubSpot\\Client\\Crm\\Deals\\Model\\Error, HTTP status code, HTTP response headers (array of strings)\n */\n public function updateWithHttpInfo($deal_id, $simple_public_object_input, $id_property = null)\n {\n $request = $this->updateRequest($deal_id, $simple_public_object_input, $id_property);\n\n try {\n $options = $this->createHttpClientOption();\n try {\n $response = $this->client->send($request, $options);\n } catch (RequestException $e) {\n throw new ApiException(\n \"[{$e->getCode()}] {$e->getMessage()}\",\n (int) $e->getCode(),\n $e->getResponse() ? $e->getResponse()->getHeaders() : null,\n $e->getResponse() ? (string) $e->getResponse()->getBody() : null\n );\n } catch (ConnectException $e) {\n throw new ApiException(\n \"[{$e->getCode()}] {$e->getMessage()}\",\n (int) $e->getCode(),\n null,\n null\n );\n }\n\n $statusCode = $response->getStatusCode();\n\n if ($statusCode < 200 || $statusCode > 299) {\n throw new ApiException(\n sprintf(\n '[%d] Error connecting to the API (%s)',\n $statusCode,\n (string) $request->getUri()\n ),\n $statusCode,\n $response->getHeaders(),\n (string) $response->getBody()\n );\n }\n\n switch($statusCode) {\n case 200:\n if ('\\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObject' === '\\SplFileObject') {\n $content = $response->getBody(); //stream goes to serializer\n } else {\n $content = (string) $response->getBody();\n }\n\n return [\n ObjectSerializer::deserialize($content, '\\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObject', []),\n $response->getStatusCode(),\n $response->getHeaders()\n ];\n default:\n if ('\\HubSpot\\Client\\Crm\\Deals\\Model\\Error' === '\\SplFileObject') {\n $content = $response->getBody(); //stream goes to serializer\n } else {\n $content = (string) $response->getBody();\n }\n\n return [\n ObjectSerializer::deserialize($content, '\\HubSpot\\Client\\Crm\\Deals\\Model\\Error', []),\n $response->getStatusCode(),\n $response->getHeaders()\n ];\n }\n\n $returnType = '\\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObject';\n if ($returnType === '\\SplFileObject') {\n $content = $response->getBody(); //stream goes to serializer\n } else {\n $content = (string) $response->getBody();\n }\n\n return [\n ObjectSerializer::deserialize($content, $returnType, []),\n $response->getStatusCode(),\n $response->getHeaders()\n ];\n\n } catch (ApiException $e) {\n switch ($e->getCode()) {\n case 200:\n $data = ObjectSerializer::deserialize(\n $e->getResponseBody(),\n '\\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObject',\n $e->getResponseHeaders()\n );\n $e->setResponseObject($data);\n break;\n default:\n $data = ObjectSerializer::deserialize(\n $e->getResponseBody(),\n '\\HubSpot\\Client\\Crm\\Deals\\Model\\Error',\n $e->getResponseHeaders()\n );\n $e->setResponseObject($data);\n break;\n }\n throw $e;\n }\n }\n\n /**\n * Operation updateAsync\n *\n * Update\n *\n * @param string $deal_id (required)\n * @param \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectInput $simple_public_object_input (required)\n * @param string $id_property The name of a property whose values are unique for this object type (optional)\n *\n * @throws \\InvalidArgumentException\n * @return \\GuzzleHttp\\Promise\\PromiseInterface\n */\n public function updateAsync($deal_id, $simple_public_object_input, $id_property = null)\n {\n return $this->updateAsyncWithHttpInfo($deal_id, $simple_public_object_input, $id_property)\n ->then(\n function ($response) {\n return $response[0];\n }\n );\n }\n\n /**\n * Operation updateAsyncWithHttpInfo\n *\n * Update\n *\n * @param string $deal_id (required)\n * @param \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectInput $simple_public_object_input (required)\n * @param string $id_property The name of a property whose values are unique for this object type (optional)\n *\n * @throws \\InvalidArgumentException\n * @return \\GuzzleHttp\\Promise\\PromiseInterface\n */\n public function updateAsyncWithHttpInfo($deal_id, $simple_public_object_input, $id_property = null)\n {\n $returnType = '\\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObject';\n $request = $this->updateRequest($deal_id, $simple_public_object_input, $id_property);\n\n return $this->client\n ->sendAsync($request, $this->createHttpClientOption())\n ->then(\n function ($response) use ($returnType) {\n if ($returnType === '\\SplFileObject') {\n $content = $response->getBody(); //stream goes to serializer\n } else {\n $content = (string) $response->getBody();\n }\n\n return [\n ObjectSerializer::deserialize($content, $returnType, []),\n $response->getStatusCode(),\n $response->getHeaders()\n ];\n },\n function ($exception) {\n $response = $exception->getResponse();\n $statusCode = $response->getStatusCode();\n throw new ApiException(\n sprintf(\n '[%d] Error connecting to the API (%s)',\n $statusCode,\n $exception->getRequest()->getUri()\n ),\n $statusCode,\n $response->getHeaders(),\n (string) $response->getBody()\n );\n }\n );\n }\n\n /**\n * Create request for operation 'update'\n *\n * @param string $deal_id (required)\n * @param \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectInput $simple_public_object_input (required)\n * @param string $id_property The name of a property whose values are unique for this object type (optional)\n *\n * @throws \\InvalidArgumentException\n * @return \\GuzzleHttp\\Psr7\\Request\n */\n public function updateRequest($deal_id, $simple_public_object_input, $id_property = null)\n {\n // verify the required parameter 'deal_id' is set\n if ($deal_id === null || (is_array($deal_id) && count($deal_id) === 0)) {\n throw new \\InvalidArgumentException(\n 'Missing the required parameter $deal_id when calling update'\n );\n }\n // verify the required parameter 'simple_public_object_input' is set\n if ($simple_public_object_input === null || (is_array($simple_public_object_input) && count($simple_public_object_input) === 0)) {\n throw new \\InvalidArgumentException(\n 'Missing the required parameter $simple_public_object_input when calling update'\n );\n }\n\n $resourcePath = '/crm/v3/objects/deals/{dealId}';\n $formParams = [];\n $queryParams = [];\n $headerParams = [];\n $httpBody = '';\n $multipart = false;\n\n // query params\n if ($id_property !== null) {\n if('form' === 'form' && is_array($id_property)) {\n foreach($id_property as $key => $value) {\n $queryParams[$key] = $value;\n }\n }\n else {\n $queryParams['idProperty'] = $id_property;\n }\n }\n\n\n // path params\n if ($deal_id !== null) {\n $resourcePath = str_replace(\n '{' . 'dealId' . '}',\n ObjectSerializer::toPathValue($deal_id),\n $resourcePath\n );\n }\n\n\n if ($multipart) {\n $headers = $this->headerSelector->selectHeadersForMultipart(\n ['application/json', '*/*']\n );\n } else {\n $headers = $this->headerSelector->selectHeaders(\n ['application/json', '*/*'],\n ['application/json']\n );\n }\n\n // for model (json/xml)\n if (isset($simple_public_object_input)) {\n if ($headers['Content-Type'] === 'application/json') {\n $httpBody = \\GuzzleHttp\\json_encode(ObjectSerializer::sanitizeForSerialization($simple_public_object_input));\n } else {\n $httpBody = $simple_public_object_input;\n }\n } elseif (count($formParams) > 0) {\n if ($multipart) {\n $multipartContents = [];\n foreach ($formParams as $formParamName => $formParamValue) {\n $formParamValueItems = is_array($formParamValue) ? $formParamValue : [$formParamValue];\n foreach ($formParamValueItems as $formParamValueItem) {\n $multipartContents[] = [\n 'name' => $formParamName,\n 'contents' => $formParamValueItem\n ];\n }\n }\n // for HTTP post (form)\n $httpBody = new MultipartStream($multipartContents);\n\n } elseif ($headers['Content-Type'] === 'application/json') {\n $httpBody = \\GuzzleHttp\\json_encode($formParams);\n\n } else {\n // for HTTP post (form)\n $httpBody = \\GuzzleHttp\\Psr7\\Query::build($formParams);\n }\n }\n\n // this endpoint requires API key authentication\n $apiKey = $this->config->getApiKeyWithPrefix('hapikey');\n if ($apiKey !== null) {\n $queryParams['hapikey'] = $apiKey;\n }\n // this endpoint requires OAuth (access token)\n if ($this->config->getAccessToken() !== null) {\n $headers['Authorization'] = 'Bearer ' . $this->config->getAccessToken();\n }\n\n $defaultHeaders = [];\n if ($this->config->getUserAgent()) {\n $defaultHeaders['User-Agent'] = $this->config->getUserAgent();\n }\n\n $headers = array_merge(\n $defaultHeaders,\n $headerParams,\n $headers\n );\n\n $query = \\GuzzleHttp\\Psr7\\Query::build($queryParams);\n return new Request(\n 'PATCH',\n $this->config->getHost() . $resourcePath . ($query ? \"?{$query}\" : ''),\n $headers,\n $httpBody\n );\n }\n\n /**\n * Create http client option\n *\n * @throws \\RuntimeException on file opening failure\n * @return array of http client options\n */\n protected function createHttpClientOption()\n {\n $options = [];\n if ($this->config->getDebug()) {\n $options[RequestOptions::DEBUG] = fopen($this->config->getDebugFile(), 'a');\n if (!$options[RequestOptions::DEBUG]) {\n throw new \\RuntimeException('Failed to open the debug file: ' . $this->config->getDebugFile());\n }\n }\n\n return $options;\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"app ~/jiminny/app, folder","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".circleci, folder","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".cursor, folder","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".github","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".sonarlint, folder","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".vscode, folder","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".windsurf, folder","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"app, sources root","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Actions, folder","depth":8,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Component, folder","depth":8,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Acl, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ActionItems, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Activity, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ActivityAnalytics, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ActivitySearch, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"AiActivityType, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"AiAutomation, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"AiCallScoring, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"AskAnything, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Dtos, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Events, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"AskAnythingPromptService.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"HistoryService.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"AskJiminnyAi, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"AWS, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"BillingManagement, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Cache, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"CoachingFeedback, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Country, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"CustomerApi, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Database, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Datadog, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"DateTime, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"DealInsights, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"DealRisks","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ElasticSearch","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Eloquent","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Encoding","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Encryption","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ES","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Faker","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"FeatureFlags","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"FFMpeg","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"FileSystem","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Gecko","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Gong","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"GuzzleHttp","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"KeyPoints","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Kiosk","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"LanguageDetection","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"LiveFeed","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Locks","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Math","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"MediaPipeline","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"MeetingBot, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"MobileSettings, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Model, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Notification, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Nudge, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ParagraphBreaker, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ParticipantSpeech, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"PartitionedCookie, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"PlaybackPage, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Playlist, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Prophet, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ProphetAi, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ProsperWorks, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Queue, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Job, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"RateLimitAware.php, abstract class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"RateLimitAwareWrapper.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"BotsQueueConstants.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Constants.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ProcessingQueueConstants.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Router, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Saml2","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"SCIM","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Seeder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Sentry","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Serializer, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Settings, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Sidekick, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Slack, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"TeamInsights, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"TimeMemoryMapper, folder","depth":9,"on_screen":false,"role_description":"text"}]...
|
5871461784004157735
|
252663743049766078
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
Editor for custom.log
Code changed:
Hide
Sync Changes
Hide This Notification
Reader Mode
Analyzing…
<?php
/**
* BasicApi
* PHP version 7.3
*
* @category Class
* @package HubSpot\Client\Crm\Deals
* @author OpenAPI Generator team
* @link [URL_WITH_CREDENTIALS] Class
* @package HubSpot\Client\Crm\Deals
* @author OpenAPI Generator team
* @link [URL_WITH_CREDENTIALS] int $hostIndex Host index (required)
*/
public function setHostIndex($hostIndex): void
{
$this->hostIndex = $hostIndex;
}
/**
* Get the host index
*
* @return int Host index
*/
public function getHostIndex()
{
return $this->hostIndex;
}
/**
* @return Configuration
*/
public function getConfig()
{
return $this->config;
}
/**
* Operation archive
*
* Archive
*
* @param string $deal_id deal_id (required)
*
* @throws \HubSpot\Client\Crm\Deals\ApiException on non-2xx response
* @throws \InvalidArgumentException
* @return void
*/
public function archive($deal_id)
{
$this->archiveWithHttpInfo($deal_id);
}
/**
* Operation archiveWithHttpInfo
*
* Archive
*
* @param string $deal_id (required)
*
* @throws \HubSpot\Client\Crm\Deals\ApiException on non-2xx response
* @throws \InvalidArgumentException
* @return array of null, HTTP status code, HTTP response headers (array of strings)
*/
public function archiveWithHttpInfo($deal_id)
{
$request = $this->archiveRequest($deal_id);
try {
$options = $this->createHttpClientOption();
try {
$response = $this->client->send($request, $options);
} catch (RequestException $e) {
throw new ApiException(
"[{$e->getCode()}] {$e->getMessage()}",
(int) $e->getCode(),
$e->getResponse() ? $e->getResponse()->getHeaders() : null,
$e->getResponse() ? (string) $e->getResponse()->getBody() : null
);
} catch (ConnectException $e) {
throw new ApiException(
"[{$e->getCode()}] {$e->getMessage()}",
(int) $e->getCode(),
null,
null
);
}
$statusCode = $response->getStatusCode();
if ($statusCode < 200 || $statusCode > 299) {
throw new ApiException(
sprintf(
'[%d] Error connecting to the API (%s)',
$statusCode,
(string) $request->getUri()
),
$statusCode,
$response->getHeaders(),
(string) $response->getBody()
);
}
return [null, $statusCode, $response->getHeaders()];
} catch (ApiException $e) {
switch ($e->getCode()) {
default:
$data = ObjectSerializer::deserialize(
$e->getResponseBody(),
'\HubSpot\Client\Crm\Deals\Model\Error',
$e->getResponseHeaders()
);
$e->setResponseObject($data);
break;
}
throw $e;
}
}
/**
* Operation archiveAsync
*
* Archive
*
* @param string $deal_id (required)
*
* @throws \InvalidArgumentException
* @return \GuzzleHttp\Promise\PromiseInterface
*/
public function archiveAsync($deal_id)
{
return $this->archiveAsyncWithHttpInfo($deal_id)
->then(
function ($response) {
return $response[0];
}
);
}
/**
* Operation archiveAsyncWithHttpInfo
*
* Archive
*
* @param string $deal_id (required)
*
* @throws \InvalidArgumentException
* @return \GuzzleHttp\Promise\PromiseInterface
*/
public function archiveAsyncWithHttpInfo($deal_id)
{
$returnType = '';
$request = $this->archiveRequest($deal_id);
return $this->client
->sendAsync($request, $this->createHttpClientOption())
->then(
function ($response) use ($returnType) {
return [null, $response->getStatusCode(), $response->getHeaders()];
},
function ($exception) {
$response = $exception->getResponse();
$statusCode = $response->getStatusCode();
throw new ApiException(
sprintf(
'[%d] Error connecting to the API (%s)',
$statusCode,
$exception->getRequest()->getUri()
),
$statusCode,
$response->getHeaders(),
(string) $response->getBody()
);
}
);
}
/**
* Create request for operation 'archive'
*
* @param string $deal_id (required)
*
* @throws \InvalidArgumentException
* @return \GuzzleHttp\Psr7\Request
*/
public function archiveRequest($deal_id)
{
// verify the required parameter 'deal_id' is set
if ($deal_id === null || (is_array($deal_id) && count($deal_id) === 0)) {
throw new \InvalidArgumentException(
'Missing the required parameter $deal_id when calling archive'
);
}
$resourcePath = '/crm/v3/objects/deals/{dealId}';
$formParams = [];
$queryParams = [];
$headerParams = [];
$httpBody = '';
$multipart = false;
// path params
if ($deal_id !== null) {
$resourcePath = str_replace(
'{' . 'dealId' . '}',
ObjectSerializer::toPathValue($deal_id),
$resourcePath
);
}
if ($multipart) {
$headers = $this->headerSelector->selectHeadersForMultipart(
['*/*']
);
} else {
$headers = $this->headerSelector->selectHeaders(
['*/*'],
[]
);
}
// for model (json/xml)
if (count($formParams) > 0) {
if ($multipart) {
$multipartContents = [];
foreach ($formParams as $formParamName => $formParamValue) {
$formParamValueItems = is_array($formParamValue) ? $formParamValue : [$formParamValue];
foreach ($formParamValueItems as $formParamValueItem) {
$multipartContents[] = [
'name' => $formParamName,
'contents' => $formParamValueItem
];
}
}
// for HTTP post (form)
$httpBody = new MultipartStream($multipartContents);
} elseif ($headers['Content-Type'] === 'application/json') {
$httpBody = \GuzzleHttp\json_encode($formParams);
} else {
// for HTTP post (form)
$httpBody = \GuzzleHttp\Psr7\Query::build($formParams);
}
}
// this endpoint requires API key authentication
$apiKey = $this->config->getApiKeyWithPrefix('hapikey');
if ($apiKey !== null) {
$queryParams['hapikey'] = $apiKey;
}
// this endpoint requires OAuth (access token)
if ($this->config->getAccessToken() !== null) {
$headers['Authorization'] = 'Bearer ' . $this->config->getAccessToken();
}
$defaultHeaders = [];
if ($this->config->getUserAgent()) {
$defaultHeaders['User-Agent'] = $this->config->getUserAgent();
}
$headers = array_merge(
$defaultHeaders,
$headerParams,
$headers
);
$query = \GuzzleHttp\Psr7\Query::build($queryParams);
return new Request(
'DELETE',
$this->config->getHost() . $resourcePath . ($query ? "?{$query}" : ''),
$headers,
$httpBody
);
}
/**
* Operation create
*
* Create
*
* @param \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectInput $simple_public_object_input simple_public_object_input (required)
*
* @throws \HubSpot\Client\Crm\Deals\ApiException on non-2xx response
* @throws \InvalidArgumentException
* @return \HubSpot\Client\Crm\Deals\Model\SimplePublicObject|\HubSpot\Client\Crm\Deals\Model\Error
*/
public function create($simple_public_object_input)
{
list($response) = $this->createWithHttpInfo($simple_public_object_input);
return $response;
}
/**
* Operation createWithHttpInfo
*
* Create
*
* @param \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectInput $simple_public_object_input (required)
*
* @throws \HubSpot\Client\Crm\Deals\ApiException on non-2xx response
* @throws \InvalidArgumentException
* @return array of \HubSpot\Client\Crm\Deals\Model\SimplePublicObject|\HubSpot\Client\Crm\Deals\Model\Error, HTTP status code, HTTP response headers (array of strings)
*/
public function createWithHttpInfo($simple_public_object_input)
{
$request = $this->createRequest($simple_public_object_input);
try {
$options = $this->createHttpClientOption();
try {
$response = $this->client->send($request, $options);
} catch (RequestException $e) {
throw new ApiException(
"[{$e->getCode()}] {$e->getMessage()}",
(int) $e->getCode(),
$e->getResponse() ? $e->getResponse()->getHeaders() : null,
$e->getResponse() ? (string) $e->getResponse()->getBody() : null
);
} catch (ConnectException $e) {
throw new ApiException(
"[{$e->getCode()}] {$e->getMessage()}",
(int) $e->getCode(),
null,
null
);
}
$statusCode = $response->getStatusCode();
if ($statusCode < 200 || $statusCode > 299) {
throw new ApiException(
sprintf(
'[%d] Error connecting to the API (%s)',
$statusCode,
(string) $request->getUri()
),
$statusCode,
$response->getHeaders(),
(string) $response->getBody()
);
}
switch($statusCode) {
case 201:
if ('\HubSpot\Client\Crm\Deals\Model\SimplePublicObject' === '\SplFileObject') {
$content = $response->getBody(); //stream goes to serializer
} else {
$content = (string) $response->getBody();
}
return [
ObjectSerializer::deserialize($content, '\HubSpot\Client\Crm\Deals\Model\SimplePublicObject', []),
$response->getStatusCode(),
$response->getHeaders()
];
default:
if ('\HubSpot\Client\Crm\Deals\Model\Error' === '\SplFileObject') {
$content = $response->getBody(); //stream goes to serializer
} else {
$content = (string) $response->getBody();
}
return [
ObjectSerializer::deserialize($content, '\HubSpot\Client\Crm\Deals\Model\Error', []),
$response->getStatusCode(),
$response->getHeaders()
];
}
$returnType = '\HubSpot\Client\Crm\Deals\Model\SimplePublicObject';
if ($returnType === '\SplFileObject') {
$content = $response->getBody(); //stream goes to serializer
} else {
$content = (string) $response->getBody();
}
return [
ObjectSerializer::deserialize($content, $returnType, []),
$response->getStatusCode(),
$response->getHeaders()
];
} catch (ApiException $e) {
switch ($e->getCode()) {
case 201:
$data = ObjectSerializer::deserialize(
$e->getResponseBody(),
'\HubSpot\Client\Crm\Deals\Model\SimplePublicObject',
$e->getResponseHeaders()
);
$e->setResponseObject($data);
break;
default:
$data = ObjectSerializer::deserialize(
$e->getResponseBody(),
'\HubSpot\Client\Crm\Deals\Model\Error',
$e->getResponseHeaders()
);
$e->setResponseObject($data);
break;
}
throw $e;
}
}
/**
* Operation createAsync
*
* Create
*
* @param \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectInput $simple_public_object_input (required)
*
* @throws \InvalidArgumentException
* @return \GuzzleHttp\Promise\PromiseInterface
*/
public function createAsync($simple_public_object_input)
{
return $this->createAsyncWithHttpInfo($simple_public_object_input)
->then(
function ($response) {
return $response[0];
}
);
}
/**
* Operation createAsyncWithHttpInfo
*
* Create
*
* @param \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectInput $simple_public_object_input (required)
*
* @throws \InvalidArgumentException
* @return \GuzzleHttp\Promise\PromiseInterface
*/
public function createAsyncWithHttpInfo($simple_public_object_input)
{
$returnType = '\HubSpot\Client\Crm\Deals\Model\SimplePublicObject';
$request = $this->createRequest($simple_public_object_input);
return $this->client
->sendAsync($request, $this->createHttpClientOption())
->then(
function ($response) use ($returnType) {
if ($returnType === '\SplFileObject') {
$content = $response->getBody(); //stream goes to serializer
} else {
$content = (string) $response->getBody();
}
return [
ObjectSerializer::deserialize($content, $returnType, []),
$response->getStatusCode(),
$response->getHeaders()
];
},
function ($exception) {
$response = $exception->getResponse();
$statusCode = $response->getStatusCode();
throw new ApiException(
sprintf(
'[%d] Error connecting to the API (%s)',
$statusCode,
$exception->getRequest()->getUri()
),
$statusCode,
$response->getHeaders(),
(string) $response->getBody()
);
}
);
}
/**
* Create request for operation 'create'
*
* @param \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectInput $simple_public_object_input (required)
*
* @throws \InvalidArgumentException
* @return \GuzzleHttp\Psr7\Request
*/
public function createRequest($simple_public_object_input)
{
// verify the required parameter 'simple_public_object_input' is set
if ($simple_public_object_input === null || (is_array($simple_public_object_input) && count($simple_public_object_input) === 0)) {
throw new \InvalidArgumentException(
'Missing the required parameter $simple_public_object_input when calling create'
);
}
$resourcePath = '/crm/v3/objects/deals';
$formParams = [];
$queryParams = [];
$headerParams = [];
$httpBody = '';
$multipart = false;
if ($multipart) {
$headers = $this->headerSelector->selectHeadersForMultipart(
['application/json', '*/*']
);
} else {
$headers = $this->headerSelector->selectHeaders(
['application/json', '*/*'],
['application/json']
);
}
// for model (json/xml)
if (isset($simple_public_object_input)) {
if ($headers['Content-Type'] === 'application/json') {
$httpBody = \GuzzleHttp\json_encode(ObjectSerializer::sanitizeForSerialization($simple_public_object_input));
} else {
$httpBody = $simple_public_object_input;
}
} elseif (count($formParams) > 0) {
if ($multipart) {
$multipartContents = [];
foreach ($formParams as $formParamName => $formParamValue) {
$formParamValueItems = is_array($formParamValue) ? $formParamValue : [$formParamValue];
foreach ($formParamValueItems as $formParamValueItem) {
$multipartContents[] = [
'name' => $formParamName,
'contents' => $formParamValueItem
];
}
}
// for HTTP post (form)
$httpBody = new MultipartStream($multipartContents);
} elseif ($headers['Content-Type'] === 'application/json') {
$httpBody = \GuzzleHttp\json_encode($formParams);
} else {
// for HTTP post (form)
$httpBody = \GuzzleHttp\Psr7\Query::build($formParams);
}
}
// this endpoint requires API key authentication
$apiKey = $this->config->getApiKeyWithPrefix('hapikey');
if ($apiKey !== null) {
$queryParams['hapikey'] = $apiKey;
}
// this endpoint requires OAuth (access token)
if ($this->config->getAccessToken() !== null) {
$headers['Authorization'] = 'Bearer ' . $this->config->getAccessToken();
}
$defaultHeaders = [];
if ($this->config->getUserAgent()) {
$defaultHeaders['User-Agent'] = $this->config->getUserAgent();
}
$headers = array_merge(
$defaultHeaders,
$headerParams,
$headers
);
$query = \GuzzleHttp\Psr7\Query::build($queryParams);
return new Request(
'POST',
$this->config->getHost() . $resourcePath . ($query ? "?{$query}" : ''),
$headers,
$httpBody
);
}
/**
* Operation getById
*
* Read
*
* @param string $deal_id deal_id (required)
* @param string[] $properties A comma separated list of the properties to be returned in the response. If any of the specified properties are not present on the requested object(s), they will be ignored. (optional)
* @param string[] $associations A comma separated list of object types to retrieve associated IDs for. If any of the specified associations do not exist, they will be ignored. (optional)
* @param bool $archived Whether to return only results that have been archived. (optional, default to false)
* @param string $id_property The name of a property whose values are unique for this object type (optional)
*
* @throws \HubSpot\Client\Crm\Deals\ApiException on non-2xx response
* @throws \InvalidArgumentException
* @return \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations|\HubSpot\Client\Crm\Deals\Model\Error
*/
public function getById($deal_id, $properties = null, $associations = null, $archived = false, $id_property = null)
{
list($response) = $this->getByIdWithHttpInfo($deal_id, $properties, $associations, $archived, $id_property);
return $response;
}
/**
* Operation getByIdWithHttpInfo
*
* Read
*
* @param string $deal_id (required)
* @param string[] $properties A comma separated list of the properties to be returned in the response. If any of the specified properties are not present on the requested object(s), they will be ignored. (optional)
* @param string[] $associations A comma separated list of object types to retrieve associated IDs for. If any of the specified associations do not exist, they will be ignored. (optional)
* @param bool $archived Whether to return only results that have been archived. (optional, default to false)
* @param string $id_property The name of a property whose values are unique for this object type (optional)
*
* @throws \HubSpot\Client\Crm\Deals\ApiException on non-2xx response
* @throws \InvalidArgumentException
* @return array of \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations|\HubSpot\Client\Crm\Deals\Model\Error, HTTP status code, HTTP response headers (array of strings)
*/
public function getByIdWithHttpInfo($deal_id, $properties = null, $associations = null, $archived = false, $id_property = null)
{
$request = $this->getByIdRequest($deal_id, $properties, $associations, $archived, $id_property);
try {
$options = $this->createHttpClientOption();
try {
$response = $this->client->send($request, $options);
} catch (RequestException $e) {
throw new ApiException(
"[{$e->getCode()}] {$e->getMessage()}",
(int) $e->getCode(),
$e->getResponse() ? $e->getResponse()->getHeaders() : null,
$e->getResponse() ? (string) $e->getResponse()->getBody() : null
);
} catch (ConnectException $e) {
throw new ApiException(
"[{$e->getCode()}] {$e->getMessage()}",
(int) $e->getCode(),
null,
null
);
}
$statusCode = $response->getStatusCode();
if ($statusCode < 200 || $statusCode > 299) {
throw new ApiException(
sprintf(
'[%d] Error connecting to the API (%s)',
$statusCode,
(string) $request->getUri()
),
$statusCode,
$response->getHeaders(),
(string) $response->getBody()
);
}
switch($statusCode) {
case 200:
if ('\HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations' === '\SplFileObject') {
$content = $response->getBody(); //stream goes to serializer
} else {
$content = (string) $response->getBody();
}
return [
ObjectSerializer::deserialize($content, '\HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations', []),
$response->getStatusCode(),
$response->getHeaders()
];
default:
if ('\HubSpot\Client\Crm\Deals\Model\Error' === '\SplFileObject') {
$content = $response->getBody(); //stream goes to serializer
} else {
$content = (string) $response->getBody();
}
return [
ObjectSerializer::deserialize($content, '\HubSpot\Client\Crm\Deals\Model\Error', []),
$response->getStatusCode(),
$response->getHeaders()
];
}
$returnType = '\HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations';
if ($returnType === '\SplFileObject') {
$content = $response->getBody(); //stream goes to serializer
} else {
$content = (string) $response->getBody();
}
return [
ObjectSerializer::deserialize($content, $returnType, []),
$response->getStatusCode(),
$response->getHeaders()
];
} catch (ApiException $e) {
switch ($e->getCode()) {
case 200:
$data = ObjectSerializer::deserialize(
$e->getResponseBody(),
'\HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations',
$e->getResponseHeaders()
);
$e->setResponseObject($data);
break;
default:
$data = ObjectSerializer::deserialize(
$e->getResponseBody(),
'\HubSpot\Client\Crm\Deals\Model\Error',
$e->getResponseHeaders()
);
$e->setResponseObject($data);
break;
}
throw $e;
}
}
/**
* Operation getByIdAsync
*
* Read
*
* @param string $deal_id (required)
* @param string[] $properties A comma separated list of the properties to be returned in the response. If any of the specified properties are not present on the requested object(s), they will be ignored. (optional)
* @param string[] $associations A comma separated list of object types to retrieve associated IDs for. If any of the specified associations do not exist, they will be ignored. (optional)
* @param bool $archived Whether to return only results that have been archived. (optional, default to false)
* @param string $id_property The name of a property whose values are unique for this object type (optional)
*
* @throws \InvalidArgumentException
* @return \GuzzleHttp\Promise\PromiseInterface
*/
public function getByIdAsync($deal_id, $properties = null, $associations = null, $archived = false, $id_property = null)
{
return $this->getByIdAsyncWithHttpInfo($deal_id, $properties, $associations, $archived, $id_property)
->then(
function ($response) {
return $response[0];
}
);
}
/**
* Operation getByIdAsyncWithHttpInfo
*
* Read
*
* @param string $deal_id (required)
* @param string[] $properties A comma separated list of the properties to be returned in the response. If any of the specified properties are not present on the requested object(s), they will be ignored. (optional)
* @param string[] $associations A comma separated list of object types to retrieve associated IDs for. If any of the specified associations do not exist, they will be ignored. (optional)
* @param bool $archived Whether to return only results that have been archived. (optional, default to false)
* @param string $id_property The name of a property whose values are unique for this object type (optional)
*
* @throws \InvalidArgumentException
* @return \GuzzleHttp\Promise\PromiseInterface
*/
public function getByIdAsyncWithHttpInfo($deal_id, $properties = null, $associations = null, $archived = false, $id_property = null)
{
$returnType = '\HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations';
$request = $this->getByIdRequest($deal_id, $properties, $associations, $archived, $id_property);
return $this->client
->sendAsync($request, $this->createHttpClientOption())
->then(
function ($response) use ($returnType) {
if ($returnType === '\SplFileObject') {
$content = $response->getBody(); //stream goes to serializer
} else {
$content = (string) $response->getBody();
}
return [
ObjectSerializer::deserialize($content, $returnType, []),
$response->getStatusCode(),
$response->getHeaders()
];
},
function ($exception) {
$response = $exception->getResponse();
$statusCode = $response->getStatusCode();
throw new ApiException(
sprintf(
'[%d] Error connecting to the API (%s)',
$statusCode,
$exception->getRequest()->getUri()
),
$statusCode,
$response->getHeaders(),
(string) $response->getBody()
);
}
);
}
/**
* Create request for operation 'getById'
*
* @param string $deal_id (required)
* @param string[] $properties A comma separated list of the properties to be returned in the response. If any of the specified properties are not present on the requested object(s), they will be ignored. (optional)
* @param string[] $associations A comma separated list of object types to retrieve associated IDs for. If any of the specified associations do not exist, they will be ignored. (optional)
* @param bool $archived Whether to return only results that have been archived. (optional, default to false)
* @param string $id_property The name of a property whose values are unique for this object type (optional)
*
* @throws \InvalidArgumentException
* @return \GuzzleHttp\Psr7\Request
*/
public function getByIdRequest($deal_id, $properties = null, $associations = null, $archived = false, $id_property = null)
{
// verify the required parameter 'deal_id' is set
if ($deal_id === null || (is_array($deal_id) && count($deal_id) === 0)) {
throw new \InvalidArgumentException(
'Missing the required parameter $deal_id when calling getById'
);
}
$resourcePath = '/crm/v3/objects/deals/{dealId}';
$formParams = [];
$queryParams = [];
$headerParams = [];
$httpBody = '';
$multipart = false;
// query params
if ($properties !== null) {
if('form' === 'form' && is_array($properties)) {
foreach($properties as $key => $value) {
$queryParams[$key] = $value;
}
}
else {
$queryParams['properties'] = $properties;
}
}
// query params
if ($associations !== null) {
if('form' === 'form' && is_array($associations)) {
foreach($associations as $key => $value) {
$queryParams[$key] = $value;
}
}
else {
$queryParams['associations'] = $associations;
}
}
// query params
if ($archived !== null) {
if('form' === 'form' && is_array($archived)) {
foreach($archived as $key => $value) {
$queryParams[$key] = $value;
}
}
else {
$queryParams['archived'] = $archived;
}
}
// query params
if ($id_property !== null) {
if('form' === 'form' && is_array($id_property)) {
foreach($id_property as $key => $value) {
$queryParams[$key] = $value;
}
}
else {
$queryParams['idProperty'] = $id_property;
}
}
// path params
if ($deal_id !== null) {
$resourcePath = str_replace(
'{' . 'dealId' . '}',
ObjectSerializer::toPathValue($deal_id),
$resourcePath
);
}
if ($multipart) {
$headers = $this->headerSelector->selectHeadersForMultipart(
['application/json', '*/*']
);
} else {
$headers = $this->headerSelector->selectHeaders(
['application/json', '*/*'],
[]
);
}
// for model (json/xml)
if (count($formParams) > 0) {
if ($multipart) {
$multipartContents = [];
foreach ($formParams as $formParamName => $formParamValue) {
$formParamValueItems = is_array($formParamValue) ? $formParamValue : [$formParamValue];
foreach ($formParamValueItems as $formParamValueItem) {
$multipartContents[] = [
'name' => $formParamName,
'contents' => $formParamValueItem
];
}
}
// for HTTP post (form)
$httpBody = new MultipartStream($multipartContents);
} elseif ($headers['Content-Type'] === 'application/json') {
$httpBody = \GuzzleHttp\json_encode($formParams);
} else {
// for HTTP post (form)
$httpBody = \GuzzleHttp\Psr7\Query::build($formParams);
}
}
// this endpoint requires API key authentication
$apiKey = $this->config->getApiKeyWithPrefix('hapikey');
if ($apiKey !== null) {
$queryParams['hapikey'] = $apiKey;
}
// this endpoint requires OAuth (access token)
if ($this->config->getAccessToken() !== null) {
$headers['Authorization'] = 'Bearer ' . $this->config->getAccessToken();
}
$defaultHeaders = [];
if ($this->config->getUserAgent()) {
$defaultHeaders['User-Agent'] = $this->config->getUserAgent();
}
$headers = array_merge(
$defaultHeaders,
$headerParams,
$headers
);
$query = \GuzzleHttp\Psr7\Query::build($queryParams);
return new Request(
'GET',
$this->config->getHost() . $resourcePath . ($query ? "?{$query}" : ''),
$headers,
$httpBody
);
}
/**
* Operation getPage
*
* List
*
* @param int $limit The maximum number of results to display per page. (optional, default to 10)
* @param string $after The paging cursor token of the last successfully read resource will be returned as the `paging.next.after` JSON property of a paged response containing more results. (optional)
* @param string[] $properties A comma separated list of the properties to be returned in the response. If any of the specified properties are not present on the requested object(s), they will be ignored. (optional)
* @param string[] $associations A comma separated list of object types to retrieve associated IDs for. If any of the specified associations do not exist, they will be ignored. (optional)
* @param bool $archived Whether to return only results that have been archived. (optional, default to false)
*
* @throws \HubSpot\Client\Crm\Deals\ApiException on non-2xx response
* @throws \InvalidArgumentException
* @return \HubSpot\Client\Crm\Deals\Model\CollectionResponseSimplePublicObjectWithAssociationsForwardPaging|\HubSpot\Client\Crm\Deals\Model\Error
*/
public function getPage($limit = 10, $after = null, $properties = null, $associations = null, $archived = false)
{
list($response) = $this->getPageWithHttpInfo($limit, $after, $properties, $associations, $archived);
return $response;
}
/**
* Operation getPageWithHttpInfo
*
* List
*
* @param int $limit The maximum number of results to display per page. (optional, default to 10)
* @param string $after The paging cursor token of the last successfully read resource will be returned as the `paging.next.after` JSON property of a paged response containing more results. (optional)
* @param string[] $properties A comma separated list of the properties to be returned in the response. If any of the specified properties are not present on the requested object(s), they will be ignored. (optional)
* @param string[] $associations A comma separated list of object types to retrieve associated IDs for. If any of the specified associations do not exist, they will be ignored. (optional)
* @param bool $archived Whether to return only results that have been archived. (optional, default to false)
*
* @throws \HubSpot\Client\Crm\Deals\ApiException on non-2xx response
* @throws \InvalidArgumentException
* @return array of \HubSpot\Client\Crm\Deals\Model\CollectionResponseSimplePublicObjectWithAssociationsForwardPaging|\HubSpot\Client\Crm\Deals\Model\Error, HTTP status code, HTTP response headers (array of strings)
*/
public function getPageWithHttpInfo($limit = 10, $after = null, $properties = null, $associations = null, $archived = false)
{
$request = $this->getPageRequest($limit, $after, $properties, $associations, $archived);
try {
$options = $this->createHttpClientOption();
try {
$response = $this->client->send($request, $options);
} catch (RequestException $e) {
throw new ApiException(
"[{$e->getCode()}] {$e->getMessage()}",
(int) $e->getCode(),
$e->getResponse() ? $e->getResponse()->getHeaders() : null,
$e->getResponse() ? (string) $e->getResponse()->getBody() : null
);
} catch (ConnectException $e) {
throw new ApiException(
"[{$e->getCode()}] {$e->getMessage()}",
(int) $e->getCode(),
null,
null
);
}
$statusCode = $response->getStatusCode();
if ($statusCode < 200 || $statusCode > 299) {
throw new ApiException(
sprintf(
'[%d] Error connecting to the API (%s)',
$statusCode,
(string) $request->getUri()
),
$statusCode,
$response->getHeaders(),
(string) $response->getBody()
);
}
switch($statusCode) {
case 200:
if ('\HubSpot\Client\Crm\Deals\Model\CollectionResponseSimplePublicObjectWithAssociationsForwardPaging' === '\SplFileObject') {
$content = $response->getBody(); //stream goes to serializer
} else {
$content = (string) $response->getBody();
}
return [
ObjectSerializer::deserialize($content, '\HubSpot\Client\Crm\Deals\Model\CollectionResponseSimplePublicObjectWithAssociationsForwardPaging', []),
$response->getStatusCode(),
$response->getHeaders()
];
default:
if ('\HubSpot\Client\Crm\Deals\Model\Error' === '\SplFileObject') {
$content = $response->getBody(); //stream goes to serializer
} else {
$content = (string) $response->getBody();
}
return [
ObjectSerializer::deserialize($content, '\HubSpot\Client\Crm\Deals\Model\Error', []),
$response->getStatusCode(),
$response->getHeaders()
];
}
$returnType = '\HubSpot\Client\Crm\Deals\Model\CollectionResponseSimplePublicObjectWithAssociationsForwardPaging';
if ($returnType === '\SplFileObject') {
$content = $response->getBody(); //stream goes to serializer
} else {
$content = (string) $response->getBody();
}
return [
ObjectSerializer::deserialize($content, $returnType, []),
$response->getStatusCode(),
$response->getHeaders()
];
} catch (ApiException $e) {
switch ($e->getCode()) {
case 200:
$data = ObjectSerializer::deserialize(
$e->getResponseBody(),
'\HubSpot\Client\Crm\Deals\Model\CollectionResponseSimplePublicObjectWithAssociationsForwardPaging',
$e->getResponseHeaders()
);
$e->setResponseObject($data);
break;
default:
$data = ObjectSerializer::deserialize(
$e->getResponseBody(),
'\HubSpot\Client\Crm\Deals\Model\Error',
$e->getResponseHeaders()
);
$e->setResponseObject($data);
break;
}
throw $e;
}
}
/**
* Operation getPageAsync
*
* List
*
* @param int $limit The maximum number of results to display per page. (optional, default to 10)
* @param string $after The paging cursor token of the last successfully read resource will be returned as the `paging.next.after` JSON property of a paged response containing more results. (optional)
* @param string[] $properties A comma separated list of the properties to be returned in the response. If any of the specified properties are not present on the requested object(s), they will be ignored. (optional)
* @param string[] $associations A comma separated list of object types to retrieve associated IDs for. If any of the specified associations do not exist, they will be ignored. (optional)
* @param bool $archived Whether to return only results that have been archived. (optional, default to false)
*
* @throws \InvalidArgumentException
* @return \GuzzleHttp\Promise\PromiseInterface
*/
public function getPageAsync($limit = 10, $after = null, $properties = null, $associations = null, $archived = false)
{
return $this->getPageAsyncWithHttpInfo($limit, $after, $properties, $associations, $archived)
->then(
function ($response) {
return $response[0];
}
);
}
/**
* Operation getPageAsyncWithHttpInfo
*
* List
*
* @param int $limit The maximum number of results to display per page. (optional, default to 10)
* @param string $after The paging cursor token of the last successfully read resource will be returned as the `paging.next.after` JSON property of a paged response containing more results. (optional)
* @param string[] $properties A comma separated list of the properties to be returned in the response. If any of the specified properties are not present on the requested object(s), they will be ignored. (optional)
* @param string[] $associations A comma separated list of object types to retrieve associated IDs for. If any of the specified associations do not exist, they will be ignored. (optional)
* @param bool $archived Whether to return only results that have been archived. (optional, default to false)
*
* @throws \InvalidArgumentException
* @return \GuzzleHttp\Promise\PromiseInterface
*/
public function getPageAsyncWithHttpInfo($limit = 10, $after = null, $properties = null, $associations = null, $archived = false)
{
$returnType = '\HubSpot\Client\Crm\Deals\Model\CollectionResponseSimplePublicObjectWithAssociationsForwardPaging';
$request = $this->getPageRequest($limit, $after, $properties, $associations, $archived);
return $this->client
->sendAsync($request, $this->createHttpClientOption())
->then(
function ($response) use ($returnType) {
if ($returnType === '\SplFileObject') {
$content = $response->getBody(); //stream goes to serializer
} else {
$content = (string) $response->getBody();
}
return [
ObjectSerializer::deserialize($content, $returnType, []),
$response->getStatusCode(),
$response->getHeaders()
];
},
function ($exception) {
$response = $exception->getResponse();
$statusCode = $response->getStatusCode();
throw new ApiException(
sprintf(
'[%d] Error connecting to the API (%s)',
$statusCode,
$exception->getRequest()->getUri()
),
$statusCode,
$response->getHeaders(),
(string) $response->getBody()
);
}
);
}
/**
* Create request for operation 'getPage'
*
* @param int $limit The maximum number of results to display per page. (optional, default to 10)
* @param string $after The paging cursor token of the last successfully read resource will be returned as the `paging.next.after` JSON property of a paged response containing more results. (optional)
* @param string[] $properties A comma separated list of the properties to be returned in the response. If any of the specified properties are not present on the requested object(s), they will be ignored. (optional)
* @param string[] $associations A comma separated list of object types to retrieve associated IDs for. If any of the specified associations do not exist, they will be ignored. (optional)
* @param bool $archived Whether to return only results that have been archived. (optional, default to false)
*
* @throws \InvalidArgumentException
* @return \GuzzleHttp\Psr7\Request
*/
public function getPageRequest($limit = 10, $after = null, $properties = null, $associations = null, $archived = false)
{
$res...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
2885
|
116
|
7
|
2026-05-07T11:46:52.362951+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778154412362_m2.jpg...
|
PhpStorm
|
faVsco.js – Hubspot/Client.php
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
PhostorimINavicatecodeFV faVsco.js?9 master ~Prole PhostorimINavicatecodeFV faVsco.js?9 master ~Proledey© BatchSyncCollector.| © RateLimitException.pnpbalchsynckealsservc clientonpcloseaDealstagesseDealFieldsService.phc) Decorateacuiviiv.one© FieldDefinitions.phpc) FieldTvpeconverter.lHubspotClientinterfac) HubspotTokenmanac(C) PavloadBuilder.php(C) RemotecrmObiectM.@ ResponseNormalize.c) Service.ohr© SyncFieldAction.php© SyncRelatedActivity 20€C) WebhookSvncBatch:v MintearationAor>M Accaccors)>D ConfigD DTO> D FiltersM Jobs› D ProspectSearchStratW service lfalts© DataClient.php© DecorateActivity.php© LocalSearch.php© LocalSearchInterface|217|© RemoteSearch.php(c) Service.php219|v D Listeners(c) ConvertLeadActivitiec) PurceLookuocache.r> M Metadata> D MiarationM Pipedrivev Salesforce… D Fields• M OnnortunitvMatchenM OnnortunitvSvneStral> M ProsnectSearchStrat• M ServiceTraits(C) Client nhn© DecorateActivity.phpT DeleteObjectsTrait.p© FieldDefinitions.php© PayloadBuilder.php© Profile.php237© QueryBuilder.phpRematchActivityOnCrmObjectDetach.phpT DeleteCrmEntityTrait.phpAddkateLimitcommand.pnp© Hubspot/Client.php X © SyncOpportunity.php© WebhookSyncBatchProcessor.phpMiddleware/RateLimited.pnpHitp/RateLimited.png© BaseRateLimiter.phpC) service.phgT SyncCrmEntitiesTrait.phpuRateLimitintenace.oho© RateLimiterInstance.phpclass Cllent extends Baseclient 1mpLements Hubspotcllentintertacepublic function getPaginatedbatabeneratorint ostotal = 0.?string dslastrecordid = nul,Generator "...* athrows DealAniExcention* athrows CrmExcentionpublic function getOpportunityByld(string Scrmid, array $fields): arraytry 1Sdeal = Sthis->getNewInstance@->crm()->deals(->basicAni@->qetById(Sdeal = Sthis-›executeRequest(fn O => Sthis-›getNewInstance()-›crmO-›deaLs()-›bas¿cAp¿()=>getBy{a(imp lode separator:",', StleldsD0 :1rlLuminate\Support\Facades \Log::channel( channel: 'custom_channel')->info('$deal • PHP_EOL • print_r($deal,} catch (DealAniEycention Se) $return: true))Sthis->l00->info@'Hubsnot Farled to fetch onnortunitv''crm_id' => $crmId,"reason' =>Se->aetMessaaeO1thoow Co.if (! $deal instanceof DealWithAssociations) {throw new CrmException( message: 'Deal not found'):notunnf'id' => $deal->getIdo'properties' => $deal-›getPropertieso'associations' => Sdeal->qetAssociations@= custom.log X = laravel.logA SF [jiminny@localhost]A HS_local [jiminny@localhost]# console [PKol)A console (eu)>0 |n| 9Support Daily - in 14 ml100% 52Thu 7 May 14:46:52AsKJiminnykepoпtAcuivityservice lest v« console [STAGING]Accept Reject• DHp filec II Try SonarQube Cloud for free II Downioad SorPAenadoen...
|
NULL
|
-7450266625833131761
|
NULL
|
click
|
ocr
|
NULL
|
PhostorimINavicatecodeFV faVsco.js?9 master ~Prole PhostorimINavicatecodeFV faVsco.js?9 master ~Proledey© BatchSyncCollector.| © RateLimitException.pnpbalchsynckealsservc clientonpcloseaDealstagesseDealFieldsService.phc) Decorateacuiviiv.one© FieldDefinitions.phpc) FieldTvpeconverter.lHubspotClientinterfac) HubspotTokenmanac(C) PavloadBuilder.php(C) RemotecrmObiectM.@ ResponseNormalize.c) Service.ohr© SyncFieldAction.php© SyncRelatedActivity 20€C) WebhookSvncBatch:v MintearationAor>M Accaccors)>D ConfigD DTO> D FiltersM Jobs› D ProspectSearchStratW service lfalts© DataClient.php© DecorateActivity.php© LocalSearch.php© LocalSearchInterface|217|© RemoteSearch.php(c) Service.php219|v D Listeners(c) ConvertLeadActivitiec) PurceLookuocache.r> M Metadata> D MiarationM Pipedrivev Salesforce… D Fields• M OnnortunitvMatchenM OnnortunitvSvneStral> M ProsnectSearchStrat• M ServiceTraits(C) Client nhn© DecorateActivity.phpT DeleteObjectsTrait.p© FieldDefinitions.php© PayloadBuilder.php© Profile.php237© QueryBuilder.phpRematchActivityOnCrmObjectDetach.phpT DeleteCrmEntityTrait.phpAddkateLimitcommand.pnp© Hubspot/Client.php X © SyncOpportunity.php© WebhookSyncBatchProcessor.phpMiddleware/RateLimited.pnpHitp/RateLimited.png© BaseRateLimiter.phpC) service.phgT SyncCrmEntitiesTrait.phpuRateLimitintenace.oho© RateLimiterInstance.phpclass Cllent extends Baseclient 1mpLements Hubspotcllentintertacepublic function getPaginatedbatabeneratorint ostotal = 0.?string dslastrecordid = nul,Generator "...* athrows DealAniExcention* athrows CrmExcentionpublic function getOpportunityByld(string Scrmid, array $fields): arraytry 1Sdeal = Sthis->getNewInstance@->crm()->deals(->basicAni@->qetById(Sdeal = Sthis-›executeRequest(fn O => Sthis-›getNewInstance()-›crmO-›deaLs()-›bas¿cAp¿()=>getBy{a(imp lode separator:",', StleldsD0 :1rlLuminate\Support\Facades \Log::channel( channel: 'custom_channel')->info('$deal • PHP_EOL • print_r($deal,} catch (DealAniEycention Se) $return: true))Sthis->l00->info@'Hubsnot Farled to fetch onnortunitv''crm_id' => $crmId,"reason' =>Se->aetMessaaeO1thoow Co.if (! $deal instanceof DealWithAssociations) {throw new CrmException( message: 'Deal not found'):notunnf'id' => $deal->getIdo'properties' => $deal-›getPropertieso'associations' => Sdeal->qetAssociations@= custom.log X = laravel.logA SF [jiminny@localhost]A HS_local [jiminny@localhost]# console [PKol)A console (eu)>0 |n| 9Support Daily - in 14 ml100% 52Thu 7 May 14:46:52AsKJiminnykepoпtAcuivityservice lest v« console [STAGING]Accept Reject• DHp filec II Try SonarQube Cloud for free II Downioad SorPAenadoen...
|
2883
|
NULL
|
NULL
|
NULL
|
|
2884
|
115
|
5
|
2026-05-07T11:46:52.258717+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778154412258_m1.jpg...
|
PhpStorm
|
faVsco.js – Hubspot/Client.php
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
iTerm2•ShellEditViewSessionScriptsProfilesWindowHe iTerm2•ShellEditViewSessionScriptsProfilesWindowHelp<$ 0(ah)Support Daily - in 14 m100% <78DEV (docker)DOCKERO 81DEV (docker)H82APP (-zsh)-zsh• X4screenpipe"configcachecompiledeventsroutesviewsjiminny-worker-processing-4:jiminny-worker-processing-4_00: stoppedjiminny-worker-processing-2: jiminny-worker-processing-2_00:stoppedjiminny-worker-processing-3:jiminny-worker-processing-3_00:stoppedjiminny-worker-processing-5:jiminny-worker-processing-5_00:stoppedjiminny-worker-processing-delayed: jiminny-worker-processing-delayed_00: stoppedworker-analytics:worker-analytics_00: stoppedworker-crm-update:worker-crm-update_00: stoppedworker-download:worker-download_00: stoppedworker-nudges:worker-nudges_00: stoppedworker-crm-sync:worker-crm-sync_00:stoppedworker-audio:worker-audio_00: stoppedworker-conferences:worker-conferences_00: stoppedworker-emails:worker-emails_00: stoppedjiminny-worker-processing-1:jiminny-worker-processing-1_00: stoppedworker:worker_00: stoppedworker-es-update:worker-es-update_00: stoppedworker-calendar:worker-calendar_00: stoppedartisan-schedule:artisan-schedule_00: stoppedartisan-schedule:artisan-schedule_00: startedjiminny-worker-processing-1:jiminny-worker-processing-1_00: startedjiminny-worker-processing-2:jiminny-worker-processing-2_00: startedjiminny-worker-processing-3:jiminny-worker-processing-3_00: startedjiminny-worker-processing-4:jiminny-worker-processing-4_00: startedjiminny-worker-processing-5:jiminny-worker-processing-5_00: startedjiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00:startedworker:worker_00: startedworker-analytics:worker-analytics_00: startedworker-audio:worker-audio_00: startedworker-calendar:worker-calendar_00: startedworker-conferences:worker-conferences_00: startedworker-crm-sync:worker-crm-sync_00:startedworker-crm-update:worker-crm-update_00: startedworker-download:worker-download_00:startedworker-emails:worker-emails_00: startedworker-es-update:worker-es-update_00: startedworker-nudges:worker-nudges_00:startedroot@docker_lamp_1:/home/jiminny# l•₴58.08ms DONE19.93ms DONE3.28ms DONE4.77ms DONE2.64ms DONE20.16ms DONE-zshThu 7 May 14:46:52181₴6DEV...
|
NULL
|
3445407981282981669
|
NULL
|
click
|
ocr
|
NULL
|
iTerm2•ShellEditViewSessionScriptsProfilesWindowHe iTerm2•ShellEditViewSessionScriptsProfilesWindowHelp<$ 0(ah)Support Daily - in 14 m100% <78DEV (docker)DOCKERO 81DEV (docker)H82APP (-zsh)-zsh• X4screenpipe"configcachecompiledeventsroutesviewsjiminny-worker-processing-4:jiminny-worker-processing-4_00: stoppedjiminny-worker-processing-2: jiminny-worker-processing-2_00:stoppedjiminny-worker-processing-3:jiminny-worker-processing-3_00:stoppedjiminny-worker-processing-5:jiminny-worker-processing-5_00:stoppedjiminny-worker-processing-delayed: jiminny-worker-processing-delayed_00: stoppedworker-analytics:worker-analytics_00: stoppedworker-crm-update:worker-crm-update_00: stoppedworker-download:worker-download_00: stoppedworker-nudges:worker-nudges_00: stoppedworker-crm-sync:worker-crm-sync_00:stoppedworker-audio:worker-audio_00: stoppedworker-conferences:worker-conferences_00: stoppedworker-emails:worker-emails_00: stoppedjiminny-worker-processing-1:jiminny-worker-processing-1_00: stoppedworker:worker_00: stoppedworker-es-update:worker-es-update_00: stoppedworker-calendar:worker-calendar_00: stoppedartisan-schedule:artisan-schedule_00: stoppedartisan-schedule:artisan-schedule_00: startedjiminny-worker-processing-1:jiminny-worker-processing-1_00: startedjiminny-worker-processing-2:jiminny-worker-processing-2_00: startedjiminny-worker-processing-3:jiminny-worker-processing-3_00: startedjiminny-worker-processing-4:jiminny-worker-processing-4_00: startedjiminny-worker-processing-5:jiminny-worker-processing-5_00: startedjiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00:startedworker:worker_00: startedworker-analytics:worker-analytics_00: startedworker-audio:worker-audio_00: startedworker-calendar:worker-calendar_00: startedworker-conferences:worker-conferences_00: startedworker-crm-sync:worker-crm-sync_00:startedworker-crm-update:worker-crm-update_00: startedworker-download:worker-download_00:startedworker-emails:worker-emails_00: startedworker-es-update:worker-es-update_00: startedworker-nudges:worker-nudges_00:startedroot@docker_lamp_1:/home/jiminny# l•₴58.08ms DONE19.93ms DONE3.28ms DONE4.77ms DONE2.64ms DONE20.16ms DONE-zshThu 7 May 14:46:52181₴6DEV...
|
2882
|
NULL
|
NULL
|
NULL
|
|
2883
|
116
|
6
|
2026-05-07T11:46:22.912927+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778154382912_m2.jpg...
|
iTerm2
|
DEV (docker)
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Last login: Thu May 7 09:44:56 on ttys006
Poetry Last login: Thu May 7 09:44:56 on ttys006
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20773-fix-automated-reports-user-pilot-tracking) $ dev
root@docker_lamp_1:/home/jiminny# php artisan crm:sync-opportunity --teamId=2 --from='2026-05-01 00:00:00'
Syncing opportunity for Hubspot
Your HubSpot account has become disconnected. Please login to Jiminny to reconnect. skipping...
root@docker_lamp_1:/home/jiminny# php artisan jiminny:token-info -A 1499 -R
----------------------------------------------------------------------------------------------------
access_token => CNeR-JHgMxIZQlNQMl8kQEwrAgwACAkUAhIJBB4BAQEDBxiCiYwCIN7Y_Qwo0qwCMhTnG549n-YtNuc1jgj-2AsLPSmw3DoyQlNQMl8kQEwrAiUACBkGawEFThwBARIBAQEEATEEAQEBAQEBAQEBAQUBEggBAQEBAYlCFPAsBNZxoDp5kAcRyeBlQoE5SM7DSgNuYTFSAFoAYABo3tj9DHAAeAA
----------------------------------------------------------------------------------------------------
access_token_expires_at => 2026-05-07 11:41:20
----------------------------------------------------------------------------------------------------
refresh_token => d5ab04e2-2109-4c0b-b513-8cba1dd54371
----------------------------------------------------------------------------------------------------
refresh_token_expires_at =>
----------------------------------------------------------------------------------------------------
root@docker_lamp_1:/home/jiminny# php artisan crm:sync-opportunity --teamId=2 --from='2026-05-01 00:00:00'
Syncing opportunity for Hubspot
Syncing opportunities modified since 2026-05-01 00:00:00...
Synced 6 opportunities.
root@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 7.40ms DONE
cache [PASSWORD_DOTS] 35.37ms DONE
compiled [PASSWORD_DOTS] 2.98ms DONE
events [PASSWORD_DOTS] 1.70ms DONE
routes [PASSWORD_DOTS] 1.64ms DONE
views [PASSWORD_DOTS] 6.48ms DONE
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-download:worker-download_00: stopped
worker-nudges:worker-nudges_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker:worker_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-emails:worker-emails_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker-audio:worker-audio_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 6.95ms DONE
cache [PASSWORD_DOTS] 9.00ms DONE
compiled [PASSWORD_DOTS] 2.63ms DONE
events [PASSWORD_DOTS] 2.35ms DONE
routes [PASSWORD_DOTS] 1.64ms DONE
views [PASSWORD_DOTS] 3.18ms DONE
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
worker-download:worker-download_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
artisan-schedule:artisan-schedule_00: stopped
worker-nudges:worker-nudges_00: stopped
worker:worker_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker-audio:worker-audio_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-emails:worker-emails_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny# php artisan crm:sync-opportunity --teamId=2 --from='2026-05-01 00:00:00'
Syncing opportunity for Hubspot
Syncing opportunities modified since 2026-05-01 00:00:00...
Synced 6 opportunities.
root@docker_lamp_1:/home/jiminny# php artisan crm:sync-opportunity --teamId=2 --opportunityId 374720564
Syncing opportunity for Hubspot
Syncing opportunity 374720564...
Synced AmirHSOpp to 5066
root@docker_lamp_1:/home/jiminny# php artisan crm:sync-contact --teamId=2 --contactId 21351
Syncing contact(s) for Hubspot
Syncing contact 21351...
Synced Lissy Newland to 464
root@docker_lamp_1:/home/jiminny#
DOCKER
Close Tab
DEV (docker)
Close Tab
APP (-zsh)
Close Tab
-zsh
Close Tab
screenpipe"
Close Tab
-zsh
Close Tab
⌥⌘1
DEV (docker)...
|
[{"role":"AXTextArea","text [{"role":"AXTextArea","text":"Last login: Thu May 7 09:44:56 on ttys006\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20773-fix-automated-reports-user-pilot-tracking) $ dev\nroot@docker_lamp_1:/home/jiminny# php artisan crm:sync-opportunity --teamId=2 --from='2026-05-01 00:00:00'\nSyncing opportunity for Hubspot\nYour HubSpot account has become disconnected. Please login to Jiminny to reconnect. skipping...\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:token-info -A 1499 -R\n----------------------------------------------------------------------------------------------------\naccess_token => CNeR-JHgMxIZQlNQMl8kQEwrAgwACAkUAhIJBB4BAQEDBxiCiYwCIN7Y_Qwo0qwCMhTnG549n-YtNuc1jgj-2AsLPSmw3DoyQlNQMl8kQEwrAiUACBkGawEFThwBARIBAQEEATEEAQEBAQEBAQEBAQUBEggBAQEBAYlCFPAsBNZxoDp5kAcRyeBlQoE5SM7DSgNuYTFSAFoAYABo3tj9DHAAeAA\n----------------------------------------------------------------------------------------------------\naccess_token_expires_at => 2026-05-07 11:41:20\n----------------------------------------------------------------------------------------------------\nrefresh_token => d5ab04e2-2109-4c0b-b513-8cba1dd54371\n----------------------------------------------------------------------------------------------------\nrefresh_token_expires_at => \n----------------------------------------------------------------------------------------------------\nroot@docker_lamp_1:/home/jiminny# php artisan crm:sync-opportunity --teamId=2 --from='2026-05-01 00:00:00'\nSyncing opportunity for Hubspot\nSyncing opportunities modified since 2026-05-01 00:00:00...\nSynced 6 opportunities.\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config ............................................................................................................................... 7.40ms DONE\n cache ............................................................................................................................... 35.37ms DONE\n compiled ............................................................................................................................. 2.98ms DONE\n events ............................................................................................................................... 1.70ms DONE\n routes ............................................................................................................................... 1.64ms DONE\n views ................................................................................................................................ 6.48ms DONE\n\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker:worker_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-emails:worker-emails_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config ............................................................................................................................... 6.95ms DONE\n cache ................................................................................................................................ 9.00ms DONE\n compiled ............................................................................................................................. 2.63ms DONE\n events ............................................................................................................................... 2.35ms DONE\n routes ............................................................................................................................... 1.64ms DONE\n views ................................................................................................................................ 3.18ms DONE\n\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-download:worker-download_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-nudges:worker-nudges_00: stopped\nworker:worker_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny# php artisan crm:sync-opportunity --teamId=2 --from='2026-05-01 00:00:00'\nSyncing opportunity for Hubspot\nSyncing opportunities modified since 2026-05-01 00:00:00...\nSynced 6 opportunities.\nroot@docker_lamp_1:/home/jiminny# php artisan crm:sync-opportunity --teamId=2 --opportunityId 374720564 \nSyncing opportunity for Hubspot\nSyncing opportunity 374720564...\nSynced AmirHSOpp to 5066\nroot@docker_lamp_1:/home/jiminny# php artisan crm:sync-contact --teamId=2 --contactId 21351\nSyncing contact(s) for Hubspot\nSyncing contact 21351...\nSynced Lissy Newland to 464\nroot@docker_lamp_1:/home/jiminny#","depth":4,"on_screen":true,"value":"Last login: Thu May 7 09:44:56 on ttys006\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20773-fix-automated-reports-user-pilot-tracking) $ dev\nroot@docker_lamp_1:/home/jiminny# php artisan crm:sync-opportunity --teamId=2 --from='2026-05-01 00:00:00'\nSyncing opportunity for Hubspot\nYour HubSpot account has become disconnected. Please login to Jiminny to reconnect. skipping...\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:token-info -A 1499 -R\n----------------------------------------------------------------------------------------------------\naccess_token => CNeR-JHgMxIZQlNQMl8kQEwrAgwACAkUAhIJBB4BAQEDBxiCiYwCIN7Y_Qwo0qwCMhTnG549n-YtNuc1jgj-2AsLPSmw3DoyQlNQMl8kQEwrAiUACBkGawEFThwBARIBAQEEATEEAQEBAQEBAQEBAQUBEggBAQEBAYlCFPAsBNZxoDp5kAcRyeBlQoE5SM7DSgNuYTFSAFoAYABo3tj9DHAAeAA\n----------------------------------------------------------------------------------------------------\naccess_token_expires_at => 2026-05-07 11:41:20\n----------------------------------------------------------------------------------------------------\nrefresh_token => d5ab04e2-2109-4c0b-b513-8cba1dd54371\n----------------------------------------------------------------------------------------------------\nrefresh_token_expires_at => \n----------------------------------------------------------------------------------------------------\nroot@docker_lamp_1:/home/jiminny# php artisan crm:sync-opportunity --teamId=2 --from='2026-05-01 00:00:00'\nSyncing opportunity for Hubspot\nSyncing opportunities modified since 2026-05-01 00:00:00...\nSynced 6 opportunities.\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config ............................................................................................................................... 7.40ms DONE\n cache ............................................................................................................................... 35.37ms DONE\n compiled ............................................................................................................................. 2.98ms DONE\n events ............................................................................................................................... 1.70ms DONE\n routes ............................................................................................................................... 1.64ms DONE\n views ................................................................................................................................ 6.48ms DONE\n\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker:worker_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-emails:worker-emails_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config ............................................................................................................................... 6.95ms DONE\n cache ................................................................................................................................ 9.00ms DONE\n compiled ............................................................................................................................. 2.63ms DONE\n events ............................................................................................................................... 2.35ms DONE\n routes ............................................................................................................................... 1.64ms DONE\n views ................................................................................................................................ 3.18ms DONE\n\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-download:worker-download_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-nudges:worker-nudges_00: stopped\nworker:worker_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny# php artisan crm:sync-opportunity --teamId=2 --from='2026-05-01 00:00:00'\nSyncing opportunity for Hubspot\nSyncing opportunities modified since 2026-05-01 00:00:00...\nSynced 6 opportunities.\nroot@docker_lamp_1:/home/jiminny# php artisan crm:sync-opportunity --teamId=2 --opportunityId 374720564 \nSyncing opportunity for Hubspot\nSyncing opportunity 374720564...\nSynced AmirHSOpp to 5066\nroot@docker_lamp_1:/home/jiminny# php artisan crm:sync-contact --teamId=2 --contactId 21351\nSyncing contact(s) for Hubspot\nSyncing contact 21351...\nSynced Lissy Newland to 464\nroot@docker_lamp_1:/home/jiminny#","is_focused":true},{"role":"AXRadioButton","text":"DOCKER","depth":2,"bounds":{"left":0.27027926,"top":1.0,"width":0.0787899,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.27227393,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"DEV (docker)","depth":2,"bounds":{"left":0.34906915,"top":1.0,"width":0.0787899,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.35106382,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"APP (-zsh)","depth":2,"bounds":{"left":0.42785904,"top":1.0,"width":0.07862367,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.42985374,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.5064827,"top":1.0,"width":0.07862367,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.5084774,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"screenpipe\"","depth":2,"bounds":{"left":0.5851064,"top":1.0,"width":0.07862367,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.58710104,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.66373,"top":1.0,"width":0.07862367,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.66572475,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"⌥⌘1","depth":1,"bounds":{"left":0.7287234,"top":1.0,"width":0.01861702,"height":-0.023144484},"on_screen":true,"automation_id":"_NS:8","role_description":"text"},{"role":"AXStaticText","text":"DEV (docker)","depth":1,"bounds":{"left":0.49534574,"top":1.0,"width":0.029920213,"height":-0.02394259},"on_screen":true,"role_description":"text"}]...
|
-3269366825163053653
|
1549507214139107588
|
click
|
accessibility
|
NULL
|
Last login: Thu May 7 09:44:56 on ttys006
Poetry Last login: Thu May 7 09:44:56 on ttys006
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20773-fix-automated-reports-user-pilot-tracking) $ dev
root@docker_lamp_1:/home/jiminny# php artisan crm:sync-opportunity --teamId=2 --from='2026-05-01 00:00:00'
Syncing opportunity for Hubspot
Your HubSpot account has become disconnected. Please login to Jiminny to reconnect. skipping...
root@docker_lamp_1:/home/jiminny# php artisan jiminny:token-info -A 1499 -R
----------------------------------------------------------------------------------------------------
access_token => CNeR-JHgMxIZQlNQMl8kQEwrAgwACAkUAhIJBB4BAQEDBxiCiYwCIN7Y_Qwo0qwCMhTnG549n-YtNuc1jgj-2AsLPSmw3DoyQlNQMl8kQEwrAiUACBkGawEFThwBARIBAQEEATEEAQEBAQEBAQEBAQUBEggBAQEBAYlCFPAsBNZxoDp5kAcRyeBlQoE5SM7DSgNuYTFSAFoAYABo3tj9DHAAeAA
----------------------------------------------------------------------------------------------------
access_token_expires_at => 2026-05-07 11:41:20
----------------------------------------------------------------------------------------------------
refresh_token => d5ab04e2-2109-4c0b-b513-8cba1dd54371
----------------------------------------------------------------------------------------------------
refresh_token_expires_at =>
----------------------------------------------------------------------------------------------------
root@docker_lamp_1:/home/jiminny# php artisan crm:sync-opportunity --teamId=2 --from='2026-05-01 00:00:00'
Syncing opportunity for Hubspot
Syncing opportunities modified since 2026-05-01 00:00:00...
Synced 6 opportunities.
root@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 7.40ms DONE
cache [PASSWORD_DOTS] 35.37ms DONE
compiled [PASSWORD_DOTS] 2.98ms DONE
events [PASSWORD_DOTS] 1.70ms DONE
routes [PASSWORD_DOTS] 1.64ms DONE
views [PASSWORD_DOTS] 6.48ms DONE
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-download:worker-download_00: stopped
worker-nudges:worker-nudges_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker:worker_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-emails:worker-emails_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker-audio:worker-audio_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 6.95ms DONE
cache [PASSWORD_DOTS] 9.00ms DONE
compiled [PASSWORD_DOTS] 2.63ms DONE
events [PASSWORD_DOTS] 2.35ms DONE
routes [PASSWORD_DOTS] 1.64ms DONE
views [PASSWORD_DOTS] 3.18ms DONE
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
worker-download:worker-download_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
artisan-schedule:artisan-schedule_00: stopped
worker-nudges:worker-nudges_00: stopped
worker:worker_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker-audio:worker-audio_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-emails:worker-emails_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny# php artisan crm:sync-opportunity --teamId=2 --from='2026-05-01 00:00:00'
Syncing opportunity for Hubspot
Syncing opportunities modified since 2026-05-01 00:00:00...
Synced 6 opportunities.
root@docker_lamp_1:/home/jiminny# php artisan crm:sync-opportunity --teamId=2 --opportunityId 374720564
Syncing opportunity for Hubspot
Syncing opportunity 374720564...
Synced AmirHSOpp to 5066
root@docker_lamp_1:/home/jiminny# php artisan crm:sync-contact --teamId=2 --contactId 21351
Syncing contact(s) for Hubspot
Syncing contact 21351...
Synced Lissy Newland to 464
root@docker_lamp_1:/home/jiminny#
DOCKER
Close Tab
DEV (docker)
Close Tab
APP (-zsh)
Close Tab
-zsh
Close Tab
screenpipe"
Close Tab
-zsh
Close Tab
⌥⌘1
DEV (docker)...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
2882
|
115
|
4
|
2026-05-07T11:46:22.594702+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778154382594_m1.jpg...
|
iTerm2
|
DEV (docker)
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Last login: Thu May 7 09:44:56 on ttys006
Poetry Last login: Thu May 7 09:44:56 on ttys006
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20773-fix-automated-reports-user-pilot-tracking) $ dev
root@docker_lamp_1:/home/jiminny# php artisan crm:sync-opportunity --teamId=2 --from='2026-05-01 00:00:00'
Syncing opportunity for Hubspot
Your HubSpot account has become disconnected. Please login to Jiminny to reconnect. skipping...
root@docker_lamp_1:/home/jiminny# php artisan jiminny:token-info -A 1499 -R
----------------------------------------------------------------------------------------------------
access_token => CNeR-JHgMxIZQlNQMl8kQEwrAgwACAkUAhIJBB4BAQEDBxiCiYwCIN7Y_Qwo0qwCMhTnG549n-YtNuc1jgj-2AsLPSmw3DoyQlNQMl8kQEwrAiUACBkGawEFThwBARIBAQEEATEEAQEBAQEBAQEBAQUBEggBAQEBAYlCFPAsBNZxoDp5kAcRyeBlQoE5SM7DSgNuYTFSAFoAYABo3tj9DHAAeAA
----------------------------------------------------------------------------------------------------
access_token_expires_at => 2026-05-07 11:41:20
----------------------------------------------------------------------------------------------------
refresh_token => d5ab04e2-2109-4c0b-b513-8cba1dd54371
----------------------------------------------------------------------------------------------------
refresh_token_expires_at =>
----------------------------------------------------------------------------------------------------
root@docker_lamp_1:/home/jiminny# php artisan crm:sync-opportunity --teamId=2 --from='2026-05-01 00:00:00'
Syncing opportunity for Hubspot
Syncing opportunities modified since 2026-05-01 00:00:00...
Synced 6 opportunities.
root@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 7.40ms DONE
cache [PASSWORD_DOTS] 35.37ms DONE
compiled [PASSWORD_DOTS] 2.98ms DONE
events [PASSWORD_DOTS] 1.70ms DONE
routes [PASSWORD_DOTS] 1.64ms DONE
views [PASSWORD_DOTS] 6.48ms DONE
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-download:worker-download_00: stopped
worker-nudges:worker-nudges_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker:worker_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-emails:worker-emails_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker-audio:worker-audio_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 6.95ms DONE
cache [PASSWORD_DOTS] 9.00ms DONE
compiled [PASSWORD_DOTS] 2.63ms DONE
events [PASSWORD_DOTS] 2.35ms DONE
routes [PASSWORD_DOTS] 1.64ms DONE
views [PASSWORD_DOTS] 3.18ms DONE
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
worker-download:worker-download_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
artisan-schedule:artisan-schedule_00: stopped
worker-nudges:worker-nudges_00: stopped
worker:worker_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker-audio:worker-audio_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-emails:worker-emails_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny# php artisan crm:sync-opportunity --teamId=2 --from='2026-05-01 00:00:00'
Syncing opportunity for Hubspot
Syncing opportunities modified since 2026-05-01 00:00:00...
Synced 6 opportunities.
root@docker_lamp_1:/home/jiminny# php artisan crm:sync-opportunity --teamId=2 --opportunityId 374720564
Syncing opportunity for Hubspot
Syncing opportunity 374720564...
Synced AmirHSOpp to 5066
root@docker_lamp_1:/home/jiminny# php artisan crm:sync-contact --teamId=2 --contactId 21351
Syncing contact(s) for Hubspot
Syncing contact 21351...
Synced Lissy Newland to 464
root@docker_lamp_1:/home/jiminny#
DOCKER
Close Tab
DEV (docker)
Close Tab
APP (-zsh)
Close Tab
-zsh
Close Tab
screenpipe"
Close Tab
-zsh
Close Tab
⌥⌘1
DEV (docker)...
|
[{"role":"AXTextArea","text [{"role":"AXTextArea","text":"Last login: Thu May 7 09:44:56 on ttys006\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20773-fix-automated-reports-user-pilot-tracking) $ dev\nroot@docker_lamp_1:/home/jiminny# php artisan crm:sync-opportunity --teamId=2 --from='2026-05-01 00:00:00'\nSyncing opportunity for Hubspot\nYour HubSpot account has become disconnected. Please login to Jiminny to reconnect. skipping...\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:token-info -A 1499 -R\n----------------------------------------------------------------------------------------------------\naccess_token => CNeR-JHgMxIZQlNQMl8kQEwrAgwACAkUAhIJBB4BAQEDBxiCiYwCIN7Y_Qwo0qwCMhTnG549n-YtNuc1jgj-2AsLPSmw3DoyQlNQMl8kQEwrAiUACBkGawEFThwBARIBAQEEATEEAQEBAQEBAQEBAQUBEggBAQEBAYlCFPAsBNZxoDp5kAcRyeBlQoE5SM7DSgNuYTFSAFoAYABo3tj9DHAAeAA\n----------------------------------------------------------------------------------------------------\naccess_token_expires_at => 2026-05-07 11:41:20\n----------------------------------------------------------------------------------------------------\nrefresh_token => d5ab04e2-2109-4c0b-b513-8cba1dd54371\n----------------------------------------------------------------------------------------------------\nrefresh_token_expires_at => \n----------------------------------------------------------------------------------------------------\nroot@docker_lamp_1:/home/jiminny# php artisan crm:sync-opportunity --teamId=2 --from='2026-05-01 00:00:00'\nSyncing opportunity for Hubspot\nSyncing opportunities modified since 2026-05-01 00:00:00...\nSynced 6 opportunities.\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config ............................................................................................................................... 7.40ms DONE\n cache ............................................................................................................................... 35.37ms DONE\n compiled ............................................................................................................................. 2.98ms DONE\n events ............................................................................................................................... 1.70ms DONE\n routes ............................................................................................................................... 1.64ms DONE\n views ................................................................................................................................ 6.48ms DONE\n\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker:worker_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-emails:worker-emails_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config ............................................................................................................................... 6.95ms DONE\n cache ................................................................................................................................ 9.00ms DONE\n compiled ............................................................................................................................. 2.63ms DONE\n events ............................................................................................................................... 2.35ms DONE\n routes ............................................................................................................................... 1.64ms DONE\n views ................................................................................................................................ 3.18ms DONE\n\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-download:worker-download_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-nudges:worker-nudges_00: stopped\nworker:worker_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny# php artisan crm:sync-opportunity --teamId=2 --from='2026-05-01 00:00:00'\nSyncing opportunity for Hubspot\nSyncing opportunities modified since 2026-05-01 00:00:00...\nSynced 6 opportunities.\nroot@docker_lamp_1:/home/jiminny# php artisan crm:sync-opportunity --teamId=2 --opportunityId 374720564 \nSyncing opportunity for Hubspot\nSyncing opportunity 374720564...\nSynced AmirHSOpp to 5066\nroot@docker_lamp_1:/home/jiminny# php artisan crm:sync-contact --teamId=2 --contactId 21351\nSyncing contact(s) for Hubspot\nSyncing contact 21351...\nSynced Lissy Newland to 464\nroot@docker_lamp_1:/home/jiminny#","depth":4,"on_screen":true,"value":"Last login: Thu May 7 09:44:56 on ttys006\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20773-fix-automated-reports-user-pilot-tracking) $ dev\nroot@docker_lamp_1:/home/jiminny# php artisan crm:sync-opportunity --teamId=2 --from='2026-05-01 00:00:00'\nSyncing opportunity for Hubspot\nYour HubSpot account has become disconnected. Please login to Jiminny to reconnect. skipping...\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:token-info -A 1499 -R\n----------------------------------------------------------------------------------------------------\naccess_token => CNeR-JHgMxIZQlNQMl8kQEwrAgwACAkUAhIJBB4BAQEDBxiCiYwCIN7Y_Qwo0qwCMhTnG549n-YtNuc1jgj-2AsLPSmw3DoyQlNQMl8kQEwrAiUACBkGawEFThwBARIBAQEEATEEAQEBAQEBAQEBAQUBEggBAQEBAYlCFPAsBNZxoDp5kAcRyeBlQoE5SM7DSgNuYTFSAFoAYABo3tj9DHAAeAA\n----------------------------------------------------------------------------------------------------\naccess_token_expires_at => 2026-05-07 11:41:20\n----------------------------------------------------------------------------------------------------\nrefresh_token => d5ab04e2-2109-4c0b-b513-8cba1dd54371\n----------------------------------------------------------------------------------------------------\nrefresh_token_expires_at => \n----------------------------------------------------------------------------------------------------\nroot@docker_lamp_1:/home/jiminny# php artisan crm:sync-opportunity --teamId=2 --from='2026-05-01 00:00:00'\nSyncing opportunity for Hubspot\nSyncing opportunities modified since 2026-05-01 00:00:00...\nSynced 6 opportunities.\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config ............................................................................................................................... 7.40ms DONE\n cache ............................................................................................................................... 35.37ms DONE\n compiled ............................................................................................................................. 2.98ms DONE\n events ............................................................................................................................... 1.70ms DONE\n routes ............................................................................................................................... 1.64ms DONE\n views ................................................................................................................................ 6.48ms DONE\n\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker:worker_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-emails:worker-emails_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config ............................................................................................................................... 6.95ms DONE\n cache ................................................................................................................................ 9.00ms DONE\n compiled ............................................................................................................................. 2.63ms DONE\n events ............................................................................................................................... 2.35ms DONE\n routes ............................................................................................................................... 1.64ms DONE\n views ................................................................................................................................ 3.18ms DONE\n\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-download:worker-download_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-nudges:worker-nudges_00: stopped\nworker:worker_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny# php artisan crm:sync-opportunity --teamId=2 --from='2026-05-01 00:00:00'\nSyncing opportunity for Hubspot\nSyncing opportunities modified since 2026-05-01 00:00:00...\nSynced 6 opportunities.\nroot@docker_lamp_1:/home/jiminny# php artisan crm:sync-opportunity --teamId=2 --opportunityId 374720564 \nSyncing opportunity for Hubspot\nSyncing opportunity 374720564...\nSynced AmirHSOpp to 5066\nroot@docker_lamp_1:/home/jiminny# php artisan crm:sync-contact --teamId=2 --contactId 21351\nSyncing contact(s) for Hubspot\nSyncing contact 21351...\nSynced Lissy Newland to 464\nroot@docker_lamp_1:/home/jiminny#","is_focused":true},{"role":"AXRadioButton","text":"DOCKER","depth":2,"bounds":{"left":0.0,"top":0.05888889,"width":0.16458334,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.004166667,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"DEV (docker)","depth":2,"bounds":{"left":0.16458334,"top":0.05888889,"width":0.16458334,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.16875,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"APP (-zsh)","depth":2,"bounds":{"left":0.32916668,"top":0.05888889,"width":0.16423611,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.33333334,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.49340278,"top":0.05888889,"width":0.16423611,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.49756944,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"screenpipe\"","depth":2,"bounds":{"left":0.6576389,"top":0.05888889,"width":0.16423611,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.66180557,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.821875,"top":0.05888889,"width":0.16423611,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.82604164,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"⌥⌘1","depth":1,"bounds":{"left":0.95763886,"top":0.032222223,"width":0.03888889,"height":0.018888889},"on_screen":true,"automation_id":"_NS:8","role_description":"text"},{"role":"AXStaticText","text":"DEV (docker)","depth":1,"bounds":{"left":0.47013888,"top":0.033333335,"width":0.0625,"height":0.017777778},"on_screen":true,"role_description":"text"}]...
|
-3269366825163053653
|
1549507214139107588
|
click
|
accessibility
|
NULL
|
Last login: Thu May 7 09:44:56 on ttys006
Poetry Last login: Thu May 7 09:44:56 on ttys006
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
Poetry could not find a pyproject.toml file in /Users/lukas/jiminny/app or its parents
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (JY-20773-fix-automated-reports-user-pilot-tracking) $ dev
root@docker_lamp_1:/home/jiminny# php artisan crm:sync-opportunity --teamId=2 --from='2026-05-01 00:00:00'
Syncing opportunity for Hubspot
Your HubSpot account has become disconnected. Please login to Jiminny to reconnect. skipping...
root@docker_lamp_1:/home/jiminny# php artisan jiminny:token-info -A 1499 -R
----------------------------------------------------------------------------------------------------
access_token => CNeR-JHgMxIZQlNQMl8kQEwrAgwACAkUAhIJBB4BAQEDBxiCiYwCIN7Y_Qwo0qwCMhTnG549n-YtNuc1jgj-2AsLPSmw3DoyQlNQMl8kQEwrAiUACBkGawEFThwBARIBAQEEATEEAQEBAQEBAQEBAQUBEggBAQEBAYlCFPAsBNZxoDp5kAcRyeBlQoE5SM7DSgNuYTFSAFoAYABo3tj9DHAAeAA
----------------------------------------------------------------------------------------------------
access_token_expires_at => 2026-05-07 11:41:20
----------------------------------------------------------------------------------------------------
refresh_token => d5ab04e2-2109-4c0b-b513-8cba1dd54371
----------------------------------------------------------------------------------------------------
refresh_token_expires_at =>
----------------------------------------------------------------------------------------------------
root@docker_lamp_1:/home/jiminny# php artisan crm:sync-opportunity --teamId=2 --from='2026-05-01 00:00:00'
Syncing opportunity for Hubspot
Syncing opportunities modified since 2026-05-01 00:00:00...
Synced 6 opportunities.
root@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 7.40ms DONE
cache [PASSWORD_DOTS] 35.37ms DONE
compiled [PASSWORD_DOTS] 2.98ms DONE
events [PASSWORD_DOTS] 1.70ms DONE
routes [PASSWORD_DOTS] 1.64ms DONE
views [PASSWORD_DOTS] 6.48ms DONE
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-download:worker-download_00: stopped
worker-nudges:worker-nudges_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker:worker_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-emails:worker-emails_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker-audio:worker-audio_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 6.95ms DONE
cache [PASSWORD_DOTS] 9.00ms DONE
compiled [PASSWORD_DOTS] 2.63ms DONE
events [PASSWORD_DOTS] 2.35ms DONE
routes [PASSWORD_DOTS] 1.64ms DONE
views [PASSWORD_DOTS] 3.18ms DONE
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
worker-download:worker-download_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
artisan-schedule:artisan-schedule_00: stopped
worker-nudges:worker-nudges_00: stopped
worker:worker_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker-audio:worker-audio_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-emails:worker-emails_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny# php artisan crm:sync-opportunity --teamId=2 --from='2026-05-01 00:00:00'
Syncing opportunity for Hubspot
Syncing opportunities modified since 2026-05-01 00:00:00...
Synced 6 opportunities.
root@docker_lamp_1:/home/jiminny# php artisan crm:sync-opportunity --teamId=2 --opportunityId 374720564
Syncing opportunity for Hubspot
Syncing opportunity 374720564...
Synced AmirHSOpp to 5066
root@docker_lamp_1:/home/jiminny# php artisan crm:sync-contact --teamId=2 --contactId 21351
Syncing contact(s) for Hubspot
Syncing contact 21351...
Synced Lissy Newland to 464
root@docker_lamp_1:/home/jiminny#
DOCKER
Close Tab
DEV (docker)
Close Tab
APP (-zsh)
Close Tab
-zsh
Close Tab
screenpipe"
Close Tab
-zsh
Close Tab
⌥⌘1
DEV (docker)...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
2881
|
116
|
5
|
2026-05-07T11:46:21.111894+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778154381111_m2.jpg...
|
PhpStorm
|
faVsco.js – Hubspot/Client.php
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
Editor for custom.log
Sync Changes
Hide This Notification
Code changed:
Hide
2
60
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot;
use HubSpot\Client\Crm\Deals\ApiException as DealApiException;
use HubSpot\Client\Crm\Contacts\ApiException as ContactApiException;
use HubSpot\Client\Crm\Companies\ApiException as CompanyApiException;
use HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations as ContactsWithAssociations;
use HubSpot\Client\Crm\Companies\Model\SimplePublicObjectWithAssociations as CompaniesWithAssociations;
use HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations as DealWithAssociations;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectInput;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectWithAssociations as ObjectWithAssociations;
use HubSpot\Client\Crm\Pipelines\Model\Error;
use HubSpot\Client\Crm\Pipelines\Model\PipelineStage;
use HubSpot\Client\Crm\Properties\Model\Property;
use HubSpot\Discovery\Discovery;
use Jiminny\Component\Utility\Service\ProviderRateLimiter;
use Jiminny\Exceptions\CrmException;
use Jiminny\Exceptions\RateLimitException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Jobs\Crm\NoteObject;
use Jiminny\Models\Crm\Field;
use Jiminny\Services\Crm\BaseClient;
use Jiminny\Services\Crm\Hubspot\DTO\Response\Owner;
use Jiminny\Services\SocialAccountService;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use SevenShores\Hubspot\Factory;
use SevenShores\Hubspot\Http\Response;
use Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService;
use Throwable;
/**
* @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
* @param callable(): T $apiCall
* @return T
*
* @throws RateLimitException
*/
private function executeRequest(callable $apiCall)
{
if (! $this->rateLimiter->canMakeRequest($this->config)) {
$retryAfter = $this->rateLimiter->requestAvailableIn($this->config);
$this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
]);
throw new RateLimitException(
'Hubspot rate limit reached for configuration ' . $this->config->getId(),
$retryAfter,
);
}
$this->rateLimiter->incrementRequestCount($this->config);
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function isHubspotRateLimit(Throwable $e): bool
{
return method_exists($e, 'getCode') && (int) $e->getCode() === 429;
}
private function parseRetryAfter(Throwable $e): int
{
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
return 10;
}
public function getMinimumApiVersion(): string
{
return self::MIN_API_VERSION;
}
public function getInstance(): Factory
{
return new Factory([
'key' => $this->accessToken,
'oauth2' => true,
'base_url' => $this->baseUrl,
]);
}
public function getNewInstance(): Discovery
{
return \HubSpot\Factory::createWithAccessToken($this->accessToken);
}
/**
* Secondly and daily limits for Hubspot API
*
* Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)
* Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds
* Daily: 250,000 | 500,000 | 1,000,000
*
* Official documentation states: The search endpoints are rate limited to five requests per second.
* Since with 5 RPS were still hitting secondly rate limits we lowered it to 4
*/
public function getPaginatedData(array $payload, string $type, int $offset = 0): array
{
$total = 0;
$lastId = null;
$rows = [];
foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {
$rows[] = $row;
}
return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
return $this->paginationService->getPaginatedDataGenerator(
$this,
$payload,
$type,
$offset,
$total,
$lastRecordId
);
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
$deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$deal ' . PHP_EOL . print_r($deal, true));
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $batchConfig['api']->read($batchReadRequest);
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
$response = $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
$response = $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
$max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // "110"
$remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // "109"
$interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // "10000"
$body = json_decode((string) $response->getBody(), true);
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));
return $response;
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function createMeeting(array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings';
return $this->makeRequest($endpoint, 'POST', $payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function updateMeeting(string $meetingId, array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings/' . $meetingId;
return $this->makeRequest($endpoint, 'PATCH', $payload);
}
/**
* @throws \Exception
*/
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string {
try {
$noteInput = new SimplePublicObjectInput([
'properties' => [
'hs_note_body' => $body,
'hubspot_owner_id' => $ownerId,
'hs_timestamp' => $timestamp,
],
]);
// Create note
$note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);
$this->getNewInstance()->crm()->objects()->associationsApi()->create(
'note',
$note->getId(),
$this->getNoteObject($noteObject),
$objectId,
$this->getNoteAssociationType($noteObject),
);
return $note->getId();
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to create note', [
'objectId' => $objectId,
'noteObject' => $noteObject->getObjectType(),
'reason' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return null;
}
public function updateEngagement(string $objectId, array $engagement, array $metadata): void
{
$this->getInstance()->engagements()->update($objectId, $engagement, $metadata);
}
public function getEngagementData(string $engagementId): array
{
$engagement = $this->getInstance()->engagements()->get($engagementId);
return $engagement->toArray();
}
public function createEngagement(array $engagement, array $associations, array $metadata): Response
{
return $this->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
}
public function isUnauthorizedException(\Exception $e): bool
{
// Check for specific HubSpot API exception types first
if ($e instanceof BadRequest) {
// BadRequest can contain 401 status codes
return $e->getCode() === 401;
}
// Check for HTTP client exceptions with status codes
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$response = $e->getResponse();
if ($response !== null) {
return $response->getStatusCode() === 401;
}
}
// Check for Guzzle HTTP exceptions
if ($e instanceof \GuzzleHttp\Exception\ClientException) {
return $e->getCode() === 401;
}
// Fallback to string matching as last resort, but be more specific
$message = strtolower($e->getMessage());
return str_contains($message, '401 unauthorized') ||
str_contains($message, 'http 401') ||
str_contains($message, 'status code 401') ||
(preg_match('/\b401\b/', $message) && str_contains($message, 'unauthorized'));
}
/**
* Validates and refreshes the access token if needed before API requests.
* This ensures long-running processes don't fail due to token expiration.
*
* @throws SocialAccountTokenInvalidException
*/
public function ensureValidToken(): void
{
if ($this->oauthAccount === null) {
return;
}
$newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);
if ($newToken !== null) {
$this->accessToken = $newToken;
}
}
public function getConfig()
{
return $this->config;
}
// returns only active (archived=false)
public function getOwners(): array
{
return $this->getNewInstance()->crm()->owners()->getAll();
}
/**
* @param bool $archived
*
* @return array<Owner>|[]
*/
public function getOwnersArchived(bool $archived = true): array
{
$endpoint = '/crm/v3/owners';
$queryParams = [
'archived' => $archived ? 'true' : 'false',
];
$queryString = http_build_query($queryParams);
$owners = [];
try {
$response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);
$responseData = $response?->toArray();
foreach ($responseData['results'] as $result) {
try {
$owners[] = Owner::create($result);
} catch (Throwable $e) {
$this->log->error('[HubSpot] Failed to process owner data', [
'result' => $result,
'error' => $e->getMessage(),
]);
continue;
}
}
} catch (Throwable $e) {
$this->log->error('HubSpot] Failed to fetch owners', [
'archived' => $archived,
'error' => $e->getMessage(),
]);
return [];
}
return $owners;
}
public function getMeeting(string $engagementId): ObjectWithAssociations
{
return $this->getNewInstance()->crm()->objects()->basicApi()
->getById('meeting', $engagementId, null, 'contact,company,deal');
}
public function deleteEngagement(string $engagementId): void
{
$this->getInstance()->engagements()->delete((int) $engagementId);
}
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array
{
$associationData = [];
$idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);
foreach ($idChunks as $idChunk) {
try {
$batchInput = new \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId();
$batchInput->setInputs(array_map(function ($id) {
$publicObjectId = new \HubSpot\Client\Crm\Associations\Model\PublicObjectId();
$publicObjectId->setId($id);
return $publicObjectId;
}, $idChunk));
$associatedObjectsData = $this
->getNewInstance()
->crm()
->associations()
->batchApi()
->read($fromObject, $toObject, $batchInput);
if ($associatedObjectsData instanceof \HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti) {
foreach ($associatedObjectsData->getResults() as $association) {
$from = $association->getFrom()->getId();
$toAssociations = $association->getTo();
if (! empty($toAssociations)) {
$associationData[$from] = array_map(function ($item) {
return $item->getId();
}, $toAssociations);
}
}
}
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to fetch associations', [
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => $e->getMessage(),
]);
}
}
return $associationData;
}
/**
* @throws \Exception
*/
private function getNoteAssociationType(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'note_to_deal',
NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it
NoteObject::Account => 'note_to_company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
/**
* @throws \Exception
*/
private function getNoteObject(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'deal',
NoteObject::Lead, NoteObject::Contact => 'contact',
NoteObject::Account => 'company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
public function addAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/create";
return $this->makeRequest($endpoint, 'POST', $payload);
}
public function removeAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/archive";
return $this->makeRequest($endpoint, 'POST', $payload);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.034242023,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: master","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"Editor for custom.log","depth":4,"bounds":{"left":0.5475399,"top":0.0726257,"width":0.44082448,"height":0.9066241},"on_screen":true,"role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.007978723,"height":0.0},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"60","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.010305851,"height":0.0},"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.00731383,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.006981383,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Component\\Utility\\Service\\ProviderRateLimiter;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n private ProviderRateLimiter $rateLimiter;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager,\n ProviderRateLimiter $rateLimiter,\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n $this->rateLimiter = $rateLimiter;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Single entry point for every HubSpot API call. Enforces the per-portal\n * rate limit configured in the rate_limits table (morphed to the current\n * Configuration) and reacts to a real 429 from HubSpot by translating it\n * into a RateLimitException carrying Retry-After.\n *\n * Wrap any outbound HubSpot call (SDK or raw HTTP) like:\n *\n * $this->executeRequest(fn () => $this->getNewInstance()->crm()->...);\n *\n * @template T\n * @param callable(): T $apiCall\n * @return T\n *\n * @throws RateLimitException\n */\n private function executeRequest(callable $apiCall)\n {\n if (! $this->rateLimiter->canMakeRequest($this->config)) {\n $retryAfter = $this->rateLimiter->requestAvailableIn($this->config);\n\n $this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n ]);\n\n throw new RateLimitException(\n 'Hubspot rate limit reached for configuration ' . $this->config->getId(),\n $retryAfter,\n );\n }\n\n $this->rateLimiter->incrementRequestCount($this->config);\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n private function isHubspotRateLimit(Throwable $e): bool\n {\n return method_exists($e, 'getCode') && (int) $e->getCode() === 429;\n }\n\n private function parseRetryAfter(Throwable $e): int\n {\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n return 10;\n }\n\n public function getMinimumApiVersion(): string\n {\n return self::MIN_API_VERSION;\n }\n\n public function getInstance(): Factory\n {\n return new Factory([\n 'key' => $this->accessToken,\n 'oauth2' => true,\n 'base_url' => $this->baseUrl,\n ]);\n }\n\n public function getNewInstance(): Discovery\n {\n return \\HubSpot\\Factory::createWithAccessToken($this->accessToken);\n }\n\n /**\n * Secondly and daily limits for Hubspot API\n *\n * Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)\n * Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds\n * Daily: 250,000 | 500,000 | 1,000,000\n *\n * Official documentation states: The search endpoints are rate limited to five requests per second.\n * Since with 5 RPS were still hitting secondly rate limits we lowered it to 4\n */\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array\n {\n $total = 0;\n $lastId = null;\n $rows = [];\n foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {\n $rows[] = $row;\n }\n\n return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n return $this->paginationService->getPaginatedDataGenerator(\n $this,\n $payload,\n $type,\n $offset,\n $total,\n $lastRecordId\n );\n }\n\n /**\n * @throws DealApiException\n * @throws CrmException\n */\n public function getOpportunityById(string $crmId, array $fields): array\n {\n try {\n $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n ));\n \n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$deal ' . PHP_EOL . print_r($deal, true));\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n $response = $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n $response = $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n\n $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // \"110\"\n $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // \"109\"\n $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // \"10000\"\n $body = json_decode((string) $response->getBody(), true);\n\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));\n\n return $response;\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Component\\Utility\\Service\\ProviderRateLimiter;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n private ProviderRateLimiter $rateLimiter;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager,\n ProviderRateLimiter $rateLimiter,\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n $this->rateLimiter = $rateLimiter;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Single entry point for every HubSpot API call. Enforces the per-portal\n * rate limit configured in the rate_limits table (morphed to the current\n * Configuration) and reacts to a real 429 from HubSpot by translating it\n * into a RateLimitException carrying Retry-After.\n *\n * Wrap any outbound HubSpot call (SDK or raw HTTP) like:\n *\n * $this->executeRequest(fn () => $this->getNewInstance()->crm()->...);\n *\n * @template T\n * @param callable(): T $apiCall\n * @return T\n *\n * @throws RateLimitException\n */\n private function executeRequest(callable $apiCall)\n {\n if (! $this->rateLimiter->canMakeRequest($this->config)) {\n $retryAfter = $this->rateLimiter->requestAvailableIn($this->config);\n\n $this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n ]);\n\n throw new RateLimitException(\n 'Hubspot rate limit reached for configuration ' . $this->config->getId(),\n $retryAfter,\n );\n }\n\n $this->rateLimiter->incrementRequestCount($this->config);\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n private function isHubspotRateLimit(Throwable $e): bool\n {\n return method_exists($e, 'getCode') && (int) $e->getCode() === 429;\n }\n\n private function parseRetryAfter(Throwable $e): int\n {\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n return 10;\n }\n\n public function getMinimumApiVersion(): string\n {\n return self::MIN_API_VERSION;\n }\n\n public function getInstance(): Factory\n {\n return new Factory([\n 'key' => $this->accessToken,\n 'oauth2' => true,\n 'base_url' => $this->baseUrl,\n ]);\n }\n\n public function getNewInstance(): Discovery\n {\n return \\HubSpot\\Factory::createWithAccessToken($this->accessToken);\n }\n\n /**\n * Secondly and daily limits for Hubspot API\n *\n * Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)\n * Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds\n * Daily: 250,000 | 500,000 | 1,000,000\n *\n * Official documentation states: The search endpoints are rate limited to five requests per second.\n * Since with 5 RPS were still hitting secondly rate limits we lowered it to 4\n */\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array\n {\n $total = 0;\n $lastId = null;\n $rows = [];\n foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {\n $rows[] = $row;\n }\n\n return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n return $this->paginationService->getPaginatedDataGenerator(\n $this,\n $payload,\n $type,\n $offset,\n $total,\n $lastRecordId\n );\n }\n\n /**\n * @throws DealApiException\n * @throws CrmException\n */\n public function getOpportunityById(string $crmId, array $fields): array\n {\n try {\n $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n ));\n \n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$deal ' . PHP_EOL . print_r($deal, true));\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n $response = $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n $response = $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n\n $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // \"110\"\n $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // \"109\"\n $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // \"10000\"\n $body = json_decode((string) $response->getBody(), true);\n\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));\n\n return $response;\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-4553803110366900145
|
5225835679589468260
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
Editor for custom.log
Sync Changes
Hide This Notification
Code changed:
Hide
2
60
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot;
use HubSpot\Client\Crm\Deals\ApiException as DealApiException;
use HubSpot\Client\Crm\Contacts\ApiException as ContactApiException;
use HubSpot\Client\Crm\Companies\ApiException as CompanyApiException;
use HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations as ContactsWithAssociations;
use HubSpot\Client\Crm\Companies\Model\SimplePublicObjectWithAssociations as CompaniesWithAssociations;
use HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations as DealWithAssociations;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectInput;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectWithAssociations as ObjectWithAssociations;
use HubSpot\Client\Crm\Pipelines\Model\Error;
use HubSpot\Client\Crm\Pipelines\Model\PipelineStage;
use HubSpot\Client\Crm\Properties\Model\Property;
use HubSpot\Discovery\Discovery;
use Jiminny\Component\Utility\Service\ProviderRateLimiter;
use Jiminny\Exceptions\CrmException;
use Jiminny\Exceptions\RateLimitException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Jobs\Crm\NoteObject;
use Jiminny\Models\Crm\Field;
use Jiminny\Services\Crm\BaseClient;
use Jiminny\Services\Crm\Hubspot\DTO\Response\Owner;
use Jiminny\Services\SocialAccountService;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use SevenShores\Hubspot\Factory;
use SevenShores\Hubspot\Http\Response;
use Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService;
use Throwable;
/**
* @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
* @param callable(): T $apiCall
* @return T
*
* @throws RateLimitException
*/
private function executeRequest(callable $apiCall)
{
if (! $this->rateLimiter->canMakeRequest($this->config)) {
$retryAfter = $this->rateLimiter->requestAvailableIn($this->config);
$this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
]);
throw new RateLimitException(
'Hubspot rate limit reached for configuration ' . $this->config->getId(),
$retryAfter,
);
}
$this->rateLimiter->incrementRequestCount($this->config);
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function isHubspotRateLimit(Throwable $e): bool
{
return method_exists($e, 'getCode') && (int) $e->getCode() === 429;
}
private function parseRetryAfter(Throwable $e): int
{
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
return 10;
}
public function getMinimumApiVersion(): string
{
return self::MIN_API_VERSION;
}
public function getInstance(): Factory
{
return new Factory([
'key' => $this->accessToken,
'oauth2' => true,
'base_url' => $this->baseUrl,
]);
}
public function getNewInstance(): Discovery
{
return \HubSpot\Factory::createWithAccessToken($this->accessToken);
}
/**
* Secondly and daily limits for Hubspot API
*
* Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)
* Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds
* Daily: 250,000 | 500,000 | 1,000,000
*
* Official documentation states: The search endpoints are rate limited to five requests per second.
* Since with 5 RPS were still hitting secondly rate limits we lowered it to 4
*/
public function getPaginatedData(array $payload, string $type, int $offset = 0): array
{
$total = 0;
$lastId = null;
$rows = [];
foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {
$rows[] = $row;
}
return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
return $this->paginationService->getPaginatedDataGenerator(
$this,
$payload,
$type,
$offset,
$total,
$lastRecordId
);
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
$deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$deal ' . PHP_EOL . print_r($deal, true));
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $batchConfig['api']->read($batchReadRequest);
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
$response = $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
$response = $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
$max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // "110"
$remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // "109"
$interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // "10000"
$body = json_decode((string) $response->getBody(), true);
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));
return $response;
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function createMeeting(array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings';
return $this->makeRequest($endpoint, 'POST', $payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function updateMeeting(string $meetingId, array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings/' . $meetingId;
return $this->makeRequest($endpoint, 'PATCH', $payload);
}
/**
* @throws \Exception
*/
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string {
try {
$noteInput = new SimplePublicObjectInput([
'properties' => [
'hs_note_body' => $body,
'hubspot_owner_id' => $ownerId,
'hs_timestamp' => $timestamp,
],
]);
// Create note
$note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);
$this->getNewInstance()->crm()->objects()->associationsApi()->create(
'note',
$note->getId(),
$this->getNoteObject($noteObject),
$objectId,
$this->getNoteAssociationType($noteObject),
);
return $note->getId();
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to create note', [
'objectId' => $objectId,
'noteObject' => $noteObject->getObjectType(),
'reason' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return null;
}
public function updateEngagement(string $objectId, array $engagement, array $metadata): void
{
$this->getInstance()->engagements()->update($objectId, $engagement, $metadata);
}
public function getEngagementData(string $engagementId): array
{
$engagement = $this->getInstance()->engagements()->get($engagementId);
return $engagement->toArray();
}
public function createEngagement(array $engagement, array $associations, array $metadata): Response
{
return $this->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
}
public function isUnauthorizedException(\Exception $e): bool
{
// Check for specific HubSpot API exception types first
if ($e instanceof BadRequest) {
// BadRequest can contain 401 status codes
return $e->getCode() === 401;
}
// Check for HTTP client exceptions with status codes
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$response = $e->getResponse();
if ($response !== null) {
return $response->getStatusCode() === 401;
}
}
// Check for Guzzle HTTP exceptions
if ($e instanceof \GuzzleHttp\Exception\ClientException) {
return $e->getCode() === 401;
}
// Fallback to string matching as last resort, but be more specific
$message = strtolower($e->getMessage());
return str_contains($message, '401 unauthorized') ||
str_contains($message, 'http 401') ||
str_contains($message, 'status code 401') ||
(preg_match('/\b401\b/', $message) && str_contains($message, 'unauthorized'));
}
/**
* Validates and refreshes the access token if needed before API requests.
* This ensures long-running processes don't fail due to token expiration.
*
* @throws SocialAccountTokenInvalidException
*/
public function ensureValidToken(): void
{
if ($this->oauthAccount === null) {
return;
}
$newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);
if ($newToken !== null) {
$this->accessToken = $newToken;
}
}
public function getConfig()
{
return $this->config;
}
// returns only active (archived=false)
public function getOwners(): array
{
return $this->getNewInstance()->crm()->owners()->getAll();
}
/**
* @param bool $archived
*
* @return array<Owner>|[]
*/
public function getOwnersArchived(bool $archived = true): array
{
$endpoint = '/crm/v3/owners';
$queryParams = [
'archived' => $archived ? 'true' : 'false',
];
$queryString = http_build_query($queryParams);
$owners = [];
try {
$response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);
$responseData = $response?->toArray();
foreach ($responseData['results'] as $result) {
try {
$owners[] = Owner::create($result);
} catch (Throwable $e) {
$this->log->error('[HubSpot] Failed to process owner data', [
'result' => $result,
'error' => $e->getMessage(),
]);
continue;
}
}
} catch (Throwable $e) {
$this->log->error('HubSpot] Failed to fetch owners', [
'archived' => $archived,
'error' => $e->getMessage(),
]);
return [];
}
return $owners;
}
public function getMeeting(string $engagementId): ObjectWithAssociations
{
return $this->getNewInstance()->crm()->objects()->basicApi()
->getById('meeting', $engagementId, null, 'contact,company,deal');
}
public function deleteEngagement(string $engagementId): void
{
$this->getInstance()->engagements()->delete((int) $engagementId);
}
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array
{
$associationData = [];
$idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);
foreach ($idChunks as $idChunk) {
try {
$batchInput = new \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId();
$batchInput->setInputs(array_map(function ($id) {
$publicObjectId = new \HubSpot\Client\Crm\Associations\Model\PublicObjectId();
$publicObjectId->setId($id);
return $publicObjectId;
}, $idChunk));
$associatedObjectsData = $this
->getNewInstance()
->crm()
->associations()
->batchApi()
->read($fromObject, $toObject, $batchInput);
if ($associatedObjectsData instanceof \HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti) {
foreach ($associatedObjectsData->getResults() as $association) {
$from = $association->getFrom()->getId();
$toAssociations = $association->getTo();
if (! empty($toAssociations)) {
$associationData[$from] = array_map(function ($item) {
return $item->getId();
}, $toAssociations);
}
}
}
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to fetch associations', [
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => $e->getMessage(),
]);
}
}
return $associationData;
}
/**
* @throws \Exception
*/
private function getNoteAssociationType(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'note_to_deal',
NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it
NoteObject::Account => 'note_to_company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
/**
* @throws \Exception
*/
private function getNoteObject(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'deal',
NoteObject::Lead, NoteObject::Contact => 'contact',
NoteObject::Account => 'company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
public function addAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/create";
return $this->makeRequest($endpoint, 'POST', $payload);
}
public function removeAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/archive";
return $this->makeRequest($endpoint, 'POST', $payload);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
2879
|
NULL
|
NULL
|
NULL
|
|
2880
|
115
|
3
|
2026-05-07T11:46:20.620262+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778154380620_m1.jpg...
|
PhpStorm
|
faVsco.js – Hubspot/Client.php
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
Editor for custom.log
Sync Changes
Hide This Notification
Code changed:
Hide
2
60
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot;
use HubSpot\Client\Crm\Deals\ApiException as DealApiException;
use HubSpot\Client\Crm\Contacts\ApiException as ContactApiException;
use HubSpot\Client\Crm\Companies\ApiException as CompanyApiException;
use HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations as ContactsWithAssociations;
use HubSpot\Client\Crm\Companies\Model\SimplePublicObjectWithAssociations as CompaniesWithAssociations;
use HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations as DealWithAssociations;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectInput;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectWithAssociations as ObjectWithAssociations;
use HubSpot\Client\Crm\Pipelines\Model\Error;
use HubSpot\Client\Crm\Pipelines\Model\PipelineStage;
use HubSpot\Client\Crm\Properties\Model\Property;
use HubSpot\Discovery\Discovery;
use Jiminny\Component\Utility\Service\ProviderRateLimiter;
use Jiminny\Exceptions\CrmException;
use Jiminny\Exceptions\RateLimitException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Jobs\Crm\NoteObject;
use Jiminny\Models\Crm\Field;
use Jiminny\Services\Crm\BaseClient;
use Jiminny\Services\Crm\Hubspot\DTO\Response\Owner;
use Jiminny\Services\SocialAccountService;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use SevenShores\Hubspot\Factory;
use SevenShores\Hubspot\Http\Response;
use Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService;
use Throwable;
/**
* @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
* @param callable(): T $apiCall
* @return T
*
* @throws RateLimitException
*/
private function executeRequest(callable $apiCall)
{
if (! $this->rateLimiter->canMakeRequest($this->config)) {
$retryAfter = $this->rateLimiter->requestAvailableIn($this->config);
$this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
]);
throw new RateLimitException(
'Hubspot rate limit reached for configuration ' . $this->config->getId(),
$retryAfter,
);
}
$this->rateLimiter->incrementRequestCount($this->config);
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function isHubspotRateLimit(Throwable $e): bool
{
return method_exists($e, 'getCode') && (int) $e->getCode() === 429;
}
private function parseRetryAfter(Throwable $e): int
{
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
return 10;
}
public function getMinimumApiVersion(): string
{
return self::MIN_API_VERSION;
}
public function getInstance(): Factory
{
return new Factory([
'key' => $this->accessToken,
'oauth2' => true,
'base_url' => $this->baseUrl,
]);
}
public function getNewInstance(): Discovery
{
return \HubSpot\Factory::createWithAccessToken($this->accessToken);
}
/**
* Secondly and daily limits for Hubspot API
*
* Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)
* Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds
* Daily: 250,000 | 500,000 | 1,000,000
*
* Official documentation states: The search endpoints are rate limited to five requests per second.
* Since with 5 RPS were still hitting secondly rate limits we lowered it to 4
*/
public function getPaginatedData(array $payload, string $type, int $offset = 0): array
{
$total = 0;
$lastId = null;
$rows = [];
foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {
$rows[] = $row;
}
return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
return $this->paginationService->getPaginatedDataGenerator(
$this,
$payload,
$type,
$offset,
$total,
$lastRecordId
);
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
$deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$deal ' . PHP_EOL . print_r($deal, true));
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $batchConfig['api']->read($batchReadRequest);
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
$response = $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
$response = $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
$max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // "110"
$remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // "109"
$interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // "10000"
$body = json_decode((string) $response->getBody(), true);
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));
return $response;
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function createMeeting(array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings';
return $this->makeRequest($endpoint, 'POST', $payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function updateMeeting(string $meetingId, array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings/' . $meetingId;
return $this->makeRequest($endpoint, 'PATCH', $payload);
}
/**
* @throws \Exception
*/
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string {
try {
$noteInput = new SimplePublicObjectInput([
'properties' => [
'hs_note_body' => $body,
'hubspot_owner_id' => $ownerId,
'hs_timestamp' => $timestamp,
],
]);
// Create note
$note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);
$this->getNewInstance()->crm()->objects()->associationsApi()->create(
'note',
$note->getId(),
$this->getNoteObject($noteObject),
$objectId,
$this->getNoteAssociationType($noteObject),
);
return $note->getId();
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to create note', [
'objectId' => $objectId,
'noteObject' => $noteObject->getObjectType(),
'reason' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return null;
}
public function updateEngagement(string $objectId, array $engagement, array $metadata): void
{
$this->getInstance()->engagements()->update($objectId, $engagement, $metadata);
}
public function getEngagementData(string $engagementId): array
{
$engagement = $this->getInstance()->engagements()->get($engagementId);
return $engagement->toArray();
}
public function createEngagement(array $engagement, array $associations, array $metadata): Response
{
return $this->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
}
public function isUnauthorizedException(\Exception $e): bool
{
// Check for specific HubSpot API exception types first
if ($e instanceof BadRequest) {
// BadRequest can contain 401 status codes
return $e->getCode() === 401;
}
// Check for HTTP client exceptions with status codes
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$response = $e->getResponse();
if ($response !== null) {
return $response->getStatusCode() === 401;
}
}
// Check for Guzzle HTTP exceptions
if ($e instanceof \GuzzleHttp\Exception\ClientException) {
return $e->getCode() === 401;
}
// Fallback to string matching as last resort, but be more specific
$message = strtolower($e->getMessage());
return str_contains($message, '401 unauthorized') ||
str_contains($message, 'http 401') ||
str_contains($message, 'status code 401') ||
(preg_match('/\b401\b/', $message) && str_contains($message, 'unauthorized'));
}
/**
* Validates and refreshes the access token if needed before API requests.
* This ensures long-running processes don't fail due to token expiration.
*
* @throws SocialAccountTokenInvalidException
*/
public function ensureValidToken(): void
{
if ($this->oauthAccount === null) {
return;
}
$newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);
if ($newToken !== null) {
$this->accessToken = $newToken;
}
}
public function getConfig()
{
return $this->config;
}
// returns only active (archived=false)
public function getOwners(): array
{
return $this->getNewInstance()->crm()->owners()->getAll();
}
/**
* @param bool $archived
*
* @return array<Owner>|[]
*/
public function getOwnersArchived(bool $archived = true): array
{
$endpoint = '/crm/v3/owners';
$queryParams = [
'archived' => $archived ? 'true' : 'false',
];
$queryString = http_build_query($queryParams);
$owners = [];
try {
$response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);
$responseData = $response?->toArray();
foreach ($responseData['results'] as $result) {
try {
$owners[] = Owner::create($result);
} catch (Throwable $e) {
$this->log->error('[HubSpot] Failed to process owner data', [
'result' => $result,
'error' => $e->getMessage(),
]);
continue;
}
}
} catch (Throwable $e) {
$this->log->error('HubSpot] Failed to fetch owners', [
'archived' => $archived,
'error' => $e->getMessage(),
]);
return [];
}
return $owners;
}
public function getMeeting(string $engagementId): ObjectWithAssociations
{
return $this->getNewInstance()->crm()->objects()->basicApi()
->getById('meeting', $engagementId, null, 'contact,company,deal');
}
public function deleteEngagement(string $engagementId): void
{
$this->getInstance()->engagements()->delete((int) $engagementId);
}
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array
{
$associationData = [];
$idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);
foreach ($idChunks as $idChunk) {
try {
$batchInput = new \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId();
$batchInput->setInputs(array_map(function ($id) {
$publicObjectId = new \HubSpot\Client\Crm\Associations\Model\PublicObjectId();
$publicObjectId->setId($id);
return $publicObjectId;
}, $idChunk));
$associatedObjectsData = $this
->getNewInstance()
->crm()
->associations()
->batchApi()
->read($fromObject, $toObject, $batchInput);
if ($associatedObjectsData instanceof \HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti) {
foreach ($associatedObjectsData->getResults() as $association) {
$from = $association->getFrom()->getId();
$toAssociations = $association->getTo();
if (! empty($toAssociations)) {
$associationData[$from] = array_map(function ($item) {
return $item->getId();
}, $toAssociations);
}
}
}
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to fetch associations', [
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => $e->getMessage(),
]);
}
}
return $associationData;
}
/**
* @throws \Exception
*/
private function getNoteAssociationType(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'note_to_deal',
NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it
NoteObject::Account => 'note_to_company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
/**
* @throws \Exception
*/
private function getNoteObject(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'deal',
NoteObject::Lead, NoteObject::Contact => 'contact',
NoteObject::Account => 'company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
public function addAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/create";
return $this->makeRequest($endpoint, 'POST', $payload);
}
public function removeAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/archive";
return $this->makeRequest($endpoint, 'POST', $payload);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"on_screen":true,"help_text":"Git Branch: master","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"Editor for custom.log","depth":4,"on_screen":true,"role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.016666668,"height":0.02111111},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"60","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.021527778,"height":0.02111111},"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.015277778,"height":0.025555555},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.014583333,"height":0.025555555},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Component\\Utility\\Service\\ProviderRateLimiter;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n private ProviderRateLimiter $rateLimiter;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager,\n ProviderRateLimiter $rateLimiter,\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n $this->rateLimiter = $rateLimiter;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Single entry point for every HubSpot API call. Enforces the per-portal\n * rate limit configured in the rate_limits table (morphed to the current\n * Configuration) and reacts to a real 429 from HubSpot by translating it\n * into a RateLimitException carrying Retry-After.\n *\n * Wrap any outbound HubSpot call (SDK or raw HTTP) like:\n *\n * $this->executeRequest(fn () => $this->getNewInstance()->crm()->...);\n *\n * @template T\n * @param callable(): T $apiCall\n * @return T\n *\n * @throws RateLimitException\n */\n private function executeRequest(callable $apiCall)\n {\n if (! $this->rateLimiter->canMakeRequest($this->config)) {\n $retryAfter = $this->rateLimiter->requestAvailableIn($this->config);\n\n $this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n ]);\n\n throw new RateLimitException(\n 'Hubspot rate limit reached for configuration ' . $this->config->getId(),\n $retryAfter,\n );\n }\n\n $this->rateLimiter->incrementRequestCount($this->config);\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n private function isHubspotRateLimit(Throwable $e): bool\n {\n return method_exists($e, 'getCode') && (int) $e->getCode() === 429;\n }\n\n private function parseRetryAfter(Throwable $e): int\n {\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n return 10;\n }\n\n public function getMinimumApiVersion(): string\n {\n return self::MIN_API_VERSION;\n }\n\n public function getInstance(): Factory\n {\n return new Factory([\n 'key' => $this->accessToken,\n 'oauth2' => true,\n 'base_url' => $this->baseUrl,\n ]);\n }\n\n public function getNewInstance(): Discovery\n {\n return \\HubSpot\\Factory::createWithAccessToken($this->accessToken);\n }\n\n /**\n * Secondly and daily limits for Hubspot API\n *\n * Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)\n * Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds\n * Daily: 250,000 | 500,000 | 1,000,000\n *\n * Official documentation states: The search endpoints are rate limited to five requests per second.\n * Since with 5 RPS were still hitting secondly rate limits we lowered it to 4\n */\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array\n {\n $total = 0;\n $lastId = null;\n $rows = [];\n foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {\n $rows[] = $row;\n }\n\n return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n return $this->paginationService->getPaginatedDataGenerator(\n $this,\n $payload,\n $type,\n $offset,\n $total,\n $lastRecordId\n );\n }\n\n /**\n * @throws DealApiException\n * @throws CrmException\n */\n public function getOpportunityById(string $crmId, array $fields): array\n {\n try {\n $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n ));\n \n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$deal ' . PHP_EOL . print_r($deal, true));\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n $response = $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n $response = $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n\n $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // \"110\"\n $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // \"109\"\n $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // \"10000\"\n $body = json_decode((string) $response->getBody(), true);\n\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));\n\n return $response;\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Component\\Utility\\Service\\ProviderRateLimiter;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n private ProviderRateLimiter $rateLimiter;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager,\n ProviderRateLimiter $rateLimiter,\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n $this->rateLimiter = $rateLimiter;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Single entry point for every HubSpot API call. Enforces the per-portal\n * rate limit configured in the rate_limits table (morphed to the current\n * Configuration) and reacts to a real 429 from HubSpot by translating it\n * into a RateLimitException carrying Retry-After.\n *\n * Wrap any outbound HubSpot call (SDK or raw HTTP) like:\n *\n * $this->executeRequest(fn () => $this->getNewInstance()->crm()->...);\n *\n * @template T\n * @param callable(): T $apiCall\n * @return T\n *\n * @throws RateLimitException\n */\n private function executeRequest(callable $apiCall)\n {\n if (! $this->rateLimiter->canMakeRequest($this->config)) {\n $retryAfter = $this->rateLimiter->requestAvailableIn($this->config);\n\n $this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n ]);\n\n throw new RateLimitException(\n 'Hubspot rate limit reached for configuration ' . $this->config->getId(),\n $retryAfter,\n );\n }\n\n $this->rateLimiter->incrementRequestCount($this->config);\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n private function isHubspotRateLimit(Throwable $e): bool\n {\n return method_exists($e, 'getCode') && (int) $e->getCode() === 429;\n }\n\n private function parseRetryAfter(Throwable $e): int\n {\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n return 10;\n }\n\n public function getMinimumApiVersion(): string\n {\n return self::MIN_API_VERSION;\n }\n\n public function getInstance(): Factory\n {\n return new Factory([\n 'key' => $this->accessToken,\n 'oauth2' => true,\n 'base_url' => $this->baseUrl,\n ]);\n }\n\n public function getNewInstance(): Discovery\n {\n return \\HubSpot\\Factory::createWithAccessToken($this->accessToken);\n }\n\n /**\n * Secondly and daily limits for Hubspot API\n *\n * Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)\n * Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds\n * Daily: 250,000 | 500,000 | 1,000,000\n *\n * Official documentation states: The search endpoints are rate limited to five requests per second.\n * Since with 5 RPS were still hitting secondly rate limits we lowered it to 4\n */\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array\n {\n $total = 0;\n $lastId = null;\n $rows = [];\n foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {\n $rows[] = $row;\n }\n\n return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n return $this->paginationService->getPaginatedDataGenerator(\n $this,\n $payload,\n $type,\n $offset,\n $total,\n $lastRecordId\n );\n }\n\n /**\n * @throws DealApiException\n * @throws CrmException\n */\n public function getOpportunityById(string $crmId, array $fields): array\n {\n try {\n $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n ));\n \n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$deal ' . PHP_EOL . print_r($deal, true));\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n $response = $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n $response = $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n\n $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // \"110\"\n $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // \"109\"\n $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // \"10000\"\n $body = json_decode((string) $response->getBody(), true);\n\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));\n\n return $response;\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-4553803110366900145
|
5225835679589468260
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
Editor for custom.log
Sync Changes
Hide This Notification
Code changed:
Hide
2
60
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot;
use HubSpot\Client\Crm\Deals\ApiException as DealApiException;
use HubSpot\Client\Crm\Contacts\ApiException as ContactApiException;
use HubSpot\Client\Crm\Companies\ApiException as CompanyApiException;
use HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations as ContactsWithAssociations;
use HubSpot\Client\Crm\Companies\Model\SimplePublicObjectWithAssociations as CompaniesWithAssociations;
use HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations as DealWithAssociations;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectInput;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectWithAssociations as ObjectWithAssociations;
use HubSpot\Client\Crm\Pipelines\Model\Error;
use HubSpot\Client\Crm\Pipelines\Model\PipelineStage;
use HubSpot\Client\Crm\Properties\Model\Property;
use HubSpot\Discovery\Discovery;
use Jiminny\Component\Utility\Service\ProviderRateLimiter;
use Jiminny\Exceptions\CrmException;
use Jiminny\Exceptions\RateLimitException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Jobs\Crm\NoteObject;
use Jiminny\Models\Crm\Field;
use Jiminny\Services\Crm\BaseClient;
use Jiminny\Services\Crm\Hubspot\DTO\Response\Owner;
use Jiminny\Services\SocialAccountService;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use SevenShores\Hubspot\Factory;
use SevenShores\Hubspot\Http\Response;
use Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService;
use Throwable;
/**
* @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
* @param callable(): T $apiCall
* @return T
*
* @throws RateLimitException
*/
private function executeRequest(callable $apiCall)
{
if (! $this->rateLimiter->canMakeRequest($this->config)) {
$retryAfter = $this->rateLimiter->requestAvailableIn($this->config);
$this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
]);
throw new RateLimitException(
'Hubspot rate limit reached for configuration ' . $this->config->getId(),
$retryAfter,
);
}
$this->rateLimiter->incrementRequestCount($this->config);
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function isHubspotRateLimit(Throwable $e): bool
{
return method_exists($e, 'getCode') && (int) $e->getCode() === 429;
}
private function parseRetryAfter(Throwable $e): int
{
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
return 10;
}
public function getMinimumApiVersion(): string
{
return self::MIN_API_VERSION;
}
public function getInstance(): Factory
{
return new Factory([
'key' => $this->accessToken,
'oauth2' => true,
'base_url' => $this->baseUrl,
]);
}
public function getNewInstance(): Discovery
{
return \HubSpot\Factory::createWithAccessToken($this->accessToken);
}
/**
* Secondly and daily limits for Hubspot API
*
* Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)
* Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds
* Daily: 250,000 | 500,000 | 1,000,000
*
* Official documentation states: The search endpoints are rate limited to five requests per second.
* Since with 5 RPS were still hitting secondly rate limits we lowered it to 4
*/
public function getPaginatedData(array $payload, string $type, int $offset = 0): array
{
$total = 0;
$lastId = null;
$rows = [];
foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {
$rows[] = $row;
}
return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
return $this->paginationService->getPaginatedDataGenerator(
$this,
$payload,
$type,
$offset,
$total,
$lastRecordId
);
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
$deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$deal ' . PHP_EOL . print_r($deal, true));
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $batchConfig['api']->read($batchReadRequest);
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
$response = $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
$response = $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
$max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // "110"
$remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // "109"
$interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // "10000"
$body = json_decode((string) $response->getBody(), true);
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));
return $response;
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function createMeeting(array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings';
return $this->makeRequest($endpoint, 'POST', $payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function updateMeeting(string $meetingId, array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings/' . $meetingId;
return $this->makeRequest($endpoint, 'PATCH', $payload);
}
/**
* @throws \Exception
*/
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string {
try {
$noteInput = new SimplePublicObjectInput([
'properties' => [
'hs_note_body' => $body,
'hubspot_owner_id' => $ownerId,
'hs_timestamp' => $timestamp,
],
]);
// Create note
$note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);
$this->getNewInstance()->crm()->objects()->associationsApi()->create(
'note',
$note->getId(),
$this->getNoteObject($noteObject),
$objectId,
$this->getNoteAssociationType($noteObject),
);
return $note->getId();
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to create note', [
'objectId' => $objectId,
'noteObject' => $noteObject->getObjectType(),
'reason' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return null;
}
public function updateEngagement(string $objectId, array $engagement, array $metadata): void
{
$this->getInstance()->engagements()->update($objectId, $engagement, $metadata);
}
public function getEngagementData(string $engagementId): array
{
$engagement = $this->getInstance()->engagements()->get($engagementId);
return $engagement->toArray();
}
public function createEngagement(array $engagement, array $associations, array $metadata): Response
{
return $this->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
}
public function isUnauthorizedException(\Exception $e): bool
{
// Check for specific HubSpot API exception types first
if ($e instanceof BadRequest) {
// BadRequest can contain 401 status codes
return $e->getCode() === 401;
}
// Check for HTTP client exceptions with status codes
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$response = $e->getResponse();
if ($response !== null) {
return $response->getStatusCode() === 401;
}
}
// Check for Guzzle HTTP exceptions
if ($e instanceof \GuzzleHttp\Exception\ClientException) {
return $e->getCode() === 401;
}
// Fallback to string matching as last resort, but be more specific
$message = strtolower($e->getMessage());
return str_contains($message, '401 unauthorized') ||
str_contains($message, 'http 401') ||
str_contains($message, 'status code 401') ||
(preg_match('/\b401\b/', $message) && str_contains($message, 'unauthorized'));
}
/**
* Validates and refreshes the access token if needed before API requests.
* This ensures long-running processes don't fail due to token expiration.
*
* @throws SocialAccountTokenInvalidException
*/
public function ensureValidToken(): void
{
if ($this->oauthAccount === null) {
return;
}
$newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);
if ($newToken !== null) {
$this->accessToken = $newToken;
}
}
public function getConfig()
{
return $this->config;
}
// returns only active (archived=false)
public function getOwners(): array
{
return $this->getNewInstance()->crm()->owners()->getAll();
}
/**
* @param bool $archived
*
* @return array<Owner>|[]
*/
public function getOwnersArchived(bool $archived = true): array
{
$endpoint = '/crm/v3/owners';
$queryParams = [
'archived' => $archived ? 'true' : 'false',
];
$queryString = http_build_query($queryParams);
$owners = [];
try {
$response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);
$responseData = $response?->toArray();
foreach ($responseData['results'] as $result) {
try {
$owners[] = Owner::create($result);
} catch (Throwable $e) {
$this->log->error('[HubSpot] Failed to process owner data', [
'result' => $result,
'error' => $e->getMessage(),
]);
continue;
}
}
} catch (Throwable $e) {
$this->log->error('HubSpot] Failed to fetch owners', [
'archived' => $archived,
'error' => $e->getMessage(),
]);
return [];
}
return $owners;
}
public function getMeeting(string $engagementId): ObjectWithAssociations
{
return $this->getNewInstance()->crm()->objects()->basicApi()
->getById('meeting', $engagementId, null, 'contact,company,deal');
}
public function deleteEngagement(string $engagementId): void
{
$this->getInstance()->engagements()->delete((int) $engagementId);
}
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array
{
$associationData = [];
$idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);
foreach ($idChunks as $idChunk) {
try {
$batchInput = new \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId();
$batchInput->setInputs(array_map(function ($id) {
$publicObjectId = new \HubSpot\Client\Crm\Associations\Model\PublicObjectId();
$publicObjectId->setId($id);
return $publicObjectId;
}, $idChunk));
$associatedObjectsData = $this
->getNewInstance()
->crm()
->associations()
->batchApi()
->read($fromObject, $toObject, $batchInput);
if ($associatedObjectsData instanceof \HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti) {
foreach ($associatedObjectsData->getResults() as $association) {
$from = $association->getFrom()->getId();
$toAssociations = $association->getTo();
if (! empty($toAssociations)) {
$associationData[$from] = array_map(function ($item) {
return $item->getId();
}, $toAssociations);
}
}
}
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to fetch associations', [
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => $e->getMessage(),
]);
}
}
return $associationData;
}
/**
* @throws \Exception
*/
private function getNoteAssociationType(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'note_to_deal',
NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it
NoteObject::Account => 'note_to_company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
/**
* @throws \Exception
*/
private function getNoteObject(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'deal',
NoteObject::Lead, NoteObject::Contact => 'contact',
NoteObject::Account => 'company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
public function addAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/create";
return $this->makeRequest($endpoint, 'POST', $payload);
}
public function removeAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/archive";
return $this->makeRequest($endpoint, 'POST', $payload);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
2878
|
NULL
|
NULL
|
NULL
|
|
2879
|
116
|
4
|
2026-05-07T11:45:35.787795+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778154335787_m2.jpg...
|
PhpStorm
|
faVsco.js – Hubspot/Client.php
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
Editor for custom.log
Sync Changes
Hide This Notification
Code changed:
Hide
2
60
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot;
use HubSpot\Client\Crm\Deals\ApiException as DealApiException;
use HubSpot\Client\Crm\Contacts\ApiException as ContactApiException;
use HubSpot\Client\Crm\Companies\ApiException as CompanyApiException;
use HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations as ContactsWithAssociations;
use HubSpot\Client\Crm\Companies\Model\SimplePublicObjectWithAssociations as CompaniesWithAssociations;
use HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations as DealWithAssociations;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectInput;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectWithAssociations as ObjectWithAssociations;
use HubSpot\Client\Crm\Pipelines\Model\Error;
use HubSpot\Client\Crm\Pipelines\Model\PipelineStage;
use HubSpot\Client\Crm\Properties\Model\Property;
use HubSpot\Discovery\Discovery;
use Jiminny\Component\Utility\Service\ProviderRateLimiter;
use Jiminny\Exceptions\CrmException;
use Jiminny\Exceptions\RateLimitException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Jobs\Crm\NoteObject;
use Jiminny\Models\Crm\Field;
use Jiminny\Services\Crm\BaseClient;
use Jiminny\Services\Crm\Hubspot\DTO\Response\Owner;
use Jiminny\Services\SocialAccountService;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use SevenShores\Hubspot\Factory;
use SevenShores\Hubspot\Http\Response;
use Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService;
use Throwable;
/**
* @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
* @param callable(): T $apiCall
* @return T
*
* @throws RateLimitException
*/
private function executeRequest(callable $apiCall)
{
if (! $this->rateLimiter->canMakeRequest($this->config)) {
$retryAfter = $this->rateLimiter->requestAvailableIn($this->config);
$this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
]);
throw new RateLimitException(
'Hubspot rate limit reached for configuration ' . $this->config->getId(),
$retryAfter,
);
}
$this->rateLimiter->incrementRequestCount($this->config);
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function isHubspotRateLimit(Throwable $e): bool
{
return method_exists($e, 'getCode') && (int) $e->getCode() === 429;
}
private function parseRetryAfter(Throwable $e): int
{
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
return 10;
}
public function getMinimumApiVersion(): string
{
return self::MIN_API_VERSION;
}
public function getInstance(): Factory
{
return new Factory([
'key' => $this->accessToken,
'oauth2' => true,
'base_url' => $this->baseUrl,
]);
}
public function getNewInstance(): Discovery
{
return \HubSpot\Factory::createWithAccessToken($this->accessToken);
}
/**
* Secondly and daily limits for Hubspot API
*
* Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)
* Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds
* Daily: 250,000 | 500,000 | 1,000,000
*
* Official documentation states: The search endpoints are rate limited to five requests per second.
* Since with 5 RPS were still hitting secondly rate limits we lowered it to 4
*/
public function getPaginatedData(array $payload, string $type, int $offset = 0): array
{
$total = 0;
$lastId = null;
$rows = [];
foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {
$rows[] = $row;
}
return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
return $this->paginationService->getPaginatedDataGenerator(
$this,
$payload,
$type,
$offset,
$total,
$lastRecordId
);
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
$deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
));
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $batchConfig['api']->read($batchReadRequest);
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
$response = $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
$response = $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
$max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // "110"
$remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // "109"
$interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // "10000"
$body = json_decode((string) $response->getBody(), true);
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));
return $response;
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function createMeeting(array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings';
return $this->makeRequest($endpoint, 'POST', $payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function updateMeeting(string $meetingId, array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings/' . $meetingId;
return $this->makeRequest($endpoint, 'PATCH', $payload);
}
/**
* @throws \Exception
*/
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string {
try {
$noteInput = new SimplePublicObjectInput([
'properties' => [
'hs_note_body' => $body,
'hubspot_owner_id' => $ownerId,
'hs_timestamp' => $timestamp,
],
]);
// Create note
$note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);
$this->getNewInstance()->crm()->objects()->associationsApi()->create(
'note',
$note->getId(),
$this->getNoteObject($noteObject),
$objectId,
$this->getNoteAssociationType($noteObject),
);
return $note->getId();
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to create note', [
'objectId' => $objectId,
'noteObject' => $noteObject->getObjectType(),
'reason' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return null;
}
public function updateEngagement(string $objectId, array $engagement, array $metadata): void
{
$this->getInstance()->engagements()->update($objectId, $engagement, $metadata);
}
public function getEngagementData(string $engagementId): array
{
$engagement = $this->getInstance()->engagements()->get($engagementId);
return $engagement->toArray();
}
public function createEngagement(array $engagement, array $associations, array $metadata): Response
{
return $this->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
}
public function isUnauthorizedException(\Exception $e): bool
{
// Check for specific HubSpot API exception types first
if ($e instanceof BadRequest) {
// BadRequest can contain 401 status codes
return $e->getCode() === 401;
}
// Check for HTTP client exceptions with status codes
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$response = $e->getResponse();
if ($response !== null) {
return $response->getStatusCode() === 401;
}
}
// Check for Guzzle HTTP exceptions
if ($e instanceof \GuzzleHttp\Exception\ClientException) {
return $e->getCode() === 401;
}
// Fallback to string matching as last resort, but be more specific
$message = strtolower($e->getMessage());
return str_contains($message, '401 unauthorized') ||
str_contains($message, 'http 401') ||
str_contains($message, 'status code 401') ||
(preg_match('/\b401\b/', $message) && str_contains($message, 'unauthorized'));
}
/**
* Validates and refreshes the access token if needed before API requests.
* This ensures long-running processes don't fail due to token expiration.
*
* @throws SocialAccountTokenInvalidException
*/
public function ensureValidToken(): void
{
if ($this->oauthAccount === null) {
return;
}
$newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);
if ($newToken !== null) {
$this->accessToken = $newToken;
}
}
public function getConfig()
{
return $this->config;
}
// returns only active (archived=false)
public function getOwners(): array
{
return $this->getNewInstance()->crm()->owners()->getAll();
}
/**
* @param bool $archived
*
* @return array<Owner>|[]
*/
public function getOwnersArchived(bool $archived = true): array
{
$endpoint = '/crm/v3/owners';
$queryParams = [
'archived' => $archived ? 'true' : 'false',
];
$queryString = http_build_query($queryParams);
$owners = [];
try {
$response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);
$responseData = $response?->toArray();
foreach ($responseData['results'] as $result) {
try {
$owners[] = Owner::create($result);
} catch (Throwable $e) {
$this->log->error('[HubSpot] Failed to process owner data', [
'result' => $result,
'error' => $e->getMessage(),
]);
continue;
}
}
} catch (Throwable $e) {
$this->log->error('HubSpot] Failed to fetch owners', [
'archived' => $archived,
'error' => $e->getMessage(),
]);
return [];
}
return $owners;
}
public function getMeeting(string $engagementId): ObjectWithAssociations
{
return $this->getNewInstance()->crm()->objects()->basicApi()
->getById('meeting', $engagementId, null, 'contact,company,deal');
}
public function deleteEngagement(string $engagementId): void
{
$this->getInstance()->engagements()->delete((int) $engagementId);
}
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array
{
$associationData = [];
$idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);
foreach ($idChunks as $idChunk) {
try {
$batchInput = new \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId();
$batchInput->setInputs(array_map(function ($id) {
$publicObjectId = new \HubSpot\Client\Crm\Associations\Model\PublicObjectId();
$publicObjectId->setId($id);
return $publicObjectId;
}, $idChunk));
$associatedObjectsData = $this
->getNewInstance()
->crm()
->associations()
->batchApi()
->read($fromObject, $toObject, $batchInput);
if ($associatedObjectsData instanceof \HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti) {
foreach ($associatedObjectsData->getResults() as $association) {
$from = $association->getFrom()->getId();
$toAssociations = $association->getTo();
if (! empty($toAssociations)) {
$associationData[$from] = array_map(function ($item) {
return $item->getId();
}, $toAssociations);
}
}
}
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to fetch associations', [
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => $e->getMessage(),
]);
}
}
return $associationData;
}
/**
* @throws \Exception
*/
private function getNoteAssociationType(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'note_to_deal',
NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it
NoteObject::Account => 'note_to_company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
/**
* @throws \Exception
*/
private function getNoteObject(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'deal',
NoteObject::Lead, NoteObject::Contact => 'contact',
NoteObject::Account => 'company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
public function addAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/create";
return $this->makeRequest($endpoint, 'POST', $payload);
}
public function removeAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/archive";
return $this->makeRequest($endpoint, 'POST', $payload);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.034242023,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: master","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"Editor for custom.log","depth":4,"bounds":{"left":0.5475399,"top":0.0726257,"width":0.44082448,"height":0.9066241},"on_screen":true,"role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.007978723,"height":0.0},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"60","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.010305851,"height":0.0},"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.00731383,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.006981383,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Component\\Utility\\Service\\ProviderRateLimiter;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n private ProviderRateLimiter $rateLimiter;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager,\n ProviderRateLimiter $rateLimiter,\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n $this->rateLimiter = $rateLimiter;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Single entry point for every HubSpot API call. Enforces the per-portal\n * rate limit configured in the rate_limits table (morphed to the current\n * Configuration) and reacts to a real 429 from HubSpot by translating it\n * into a RateLimitException carrying Retry-After.\n *\n * Wrap any outbound HubSpot call (SDK or raw HTTP) like:\n *\n * $this->executeRequest(fn () => $this->getNewInstance()->crm()->...);\n *\n * @template T\n * @param callable(): T $apiCall\n * @return T\n *\n * @throws RateLimitException\n */\n private function executeRequest(callable $apiCall)\n {\n if (! $this->rateLimiter->canMakeRequest($this->config)) {\n $retryAfter = $this->rateLimiter->requestAvailableIn($this->config);\n\n $this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n ]);\n\n throw new RateLimitException(\n 'Hubspot rate limit reached for configuration ' . $this->config->getId(),\n $retryAfter,\n );\n }\n\n $this->rateLimiter->incrementRequestCount($this->config);\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n private function isHubspotRateLimit(Throwable $e): bool\n {\n return method_exists($e, 'getCode') && (int) $e->getCode() === 429;\n }\n\n private function parseRetryAfter(Throwable $e): int\n {\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n return 10;\n }\n\n public function getMinimumApiVersion(): string\n {\n return self::MIN_API_VERSION;\n }\n\n public function getInstance(): Factory\n {\n return new Factory([\n 'key' => $this->accessToken,\n 'oauth2' => true,\n 'base_url' => $this->baseUrl,\n ]);\n }\n\n public function getNewInstance(): Discovery\n {\n return \\HubSpot\\Factory::createWithAccessToken($this->accessToken);\n }\n\n /**\n * Secondly and daily limits for Hubspot API\n *\n * Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)\n * Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds\n * Daily: 250,000 | 500,000 | 1,000,000\n *\n * Official documentation states: The search endpoints are rate limited to five requests per second.\n * Since with 5 RPS were still hitting secondly rate limits we lowered it to 4\n */\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array\n {\n $total = 0;\n $lastId = null;\n $rows = [];\n foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {\n $rows[] = $row;\n }\n\n return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n return $this->paginationService->getPaginatedDataGenerator(\n $this,\n $payload,\n $type,\n $offset,\n $total,\n $lastRecordId\n );\n }\n\n /**\n * @throws DealApiException\n * @throws CrmException\n */\n public function getOpportunityById(string $crmId, array $fields): array\n {\n try {\n $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n ));\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n $response = $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n $response = $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n\n $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // \"110\"\n $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // \"109\"\n $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // \"10000\"\n $body = json_decode((string) $response->getBody(), true);\n\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));\n\n return $response;\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Component\\Utility\\Service\\ProviderRateLimiter;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n private ProviderRateLimiter $rateLimiter;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager,\n ProviderRateLimiter $rateLimiter,\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n $this->rateLimiter = $rateLimiter;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Single entry point for every HubSpot API call. Enforces the per-portal\n * rate limit configured in the rate_limits table (morphed to the current\n * Configuration) and reacts to a real 429 from HubSpot by translating it\n * into a RateLimitException carrying Retry-After.\n *\n * Wrap any outbound HubSpot call (SDK or raw HTTP) like:\n *\n * $this->executeRequest(fn () => $this->getNewInstance()->crm()->...);\n *\n * @template T\n * @param callable(): T $apiCall\n * @return T\n *\n * @throws RateLimitException\n */\n private function executeRequest(callable $apiCall)\n {\n if (! $this->rateLimiter->canMakeRequest($this->config)) {\n $retryAfter = $this->rateLimiter->requestAvailableIn($this->config);\n\n $this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n ]);\n\n throw new RateLimitException(\n 'Hubspot rate limit reached for configuration ' . $this->config->getId(),\n $retryAfter,\n );\n }\n\n $this->rateLimiter->incrementRequestCount($this->config);\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n private function isHubspotRateLimit(Throwable $e): bool\n {\n return method_exists($e, 'getCode') && (int) $e->getCode() === 429;\n }\n\n private function parseRetryAfter(Throwable $e): int\n {\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n return 10;\n }\n\n public function getMinimumApiVersion(): string\n {\n return self::MIN_API_VERSION;\n }\n\n public function getInstance(): Factory\n {\n return new Factory([\n 'key' => $this->accessToken,\n 'oauth2' => true,\n 'base_url' => $this->baseUrl,\n ]);\n }\n\n public function getNewInstance(): Discovery\n {\n return \\HubSpot\\Factory::createWithAccessToken($this->accessToken);\n }\n\n /**\n * Secondly and daily limits for Hubspot API\n *\n * Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)\n * Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds\n * Daily: 250,000 | 500,000 | 1,000,000\n *\n * Official documentation states: The search endpoints are rate limited to five requests per second.\n * Since with 5 RPS were still hitting secondly rate limits we lowered it to 4\n */\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array\n {\n $total = 0;\n $lastId = null;\n $rows = [];\n foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {\n $rows[] = $row;\n }\n\n return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n return $this->paginationService->getPaginatedDataGenerator(\n $this,\n $payload,\n $type,\n $offset,\n $total,\n $lastRecordId\n );\n }\n\n /**\n * @throws DealApiException\n * @throws CrmException\n */\n public function getOpportunityById(string $crmId, array $fields): array\n {\n try {\n $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n ));\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n $response = $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n $response = $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n\n $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // \"110\"\n $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // \"109\"\n $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // \"10000\"\n $body = json_decode((string) $response->getBody(), true);\n\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));\n\n return $response;\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-188086479456983350
|
6378757184196315236
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
Editor for custom.log
Sync Changes
Hide This Notification
Code changed:
Hide
2
60
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot;
use HubSpot\Client\Crm\Deals\ApiException as DealApiException;
use HubSpot\Client\Crm\Contacts\ApiException as ContactApiException;
use HubSpot\Client\Crm\Companies\ApiException as CompanyApiException;
use HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations as ContactsWithAssociations;
use HubSpot\Client\Crm\Companies\Model\SimplePublicObjectWithAssociations as CompaniesWithAssociations;
use HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations as DealWithAssociations;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectInput;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectWithAssociations as ObjectWithAssociations;
use HubSpot\Client\Crm\Pipelines\Model\Error;
use HubSpot\Client\Crm\Pipelines\Model\PipelineStage;
use HubSpot\Client\Crm\Properties\Model\Property;
use HubSpot\Discovery\Discovery;
use Jiminny\Component\Utility\Service\ProviderRateLimiter;
use Jiminny\Exceptions\CrmException;
use Jiminny\Exceptions\RateLimitException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Jobs\Crm\NoteObject;
use Jiminny\Models\Crm\Field;
use Jiminny\Services\Crm\BaseClient;
use Jiminny\Services\Crm\Hubspot\DTO\Response\Owner;
use Jiminny\Services\SocialAccountService;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use SevenShores\Hubspot\Factory;
use SevenShores\Hubspot\Http\Response;
use Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService;
use Throwable;
/**
* @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
* @param callable(): T $apiCall
* @return T
*
* @throws RateLimitException
*/
private function executeRequest(callable $apiCall)
{
if (! $this->rateLimiter->canMakeRequest($this->config)) {
$retryAfter = $this->rateLimiter->requestAvailableIn($this->config);
$this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
]);
throw new RateLimitException(
'Hubspot rate limit reached for configuration ' . $this->config->getId(),
$retryAfter,
);
}
$this->rateLimiter->incrementRequestCount($this->config);
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function isHubspotRateLimit(Throwable $e): bool
{
return method_exists($e, 'getCode') && (int) $e->getCode() === 429;
}
private function parseRetryAfter(Throwable $e): int
{
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
return 10;
}
public function getMinimumApiVersion(): string
{
return self::MIN_API_VERSION;
}
public function getInstance(): Factory
{
return new Factory([
'key' => $this->accessToken,
'oauth2' => true,
'base_url' => $this->baseUrl,
]);
}
public function getNewInstance(): Discovery
{
return \HubSpot\Factory::createWithAccessToken($this->accessToken);
}
/**
* Secondly and daily limits for Hubspot API
*
* Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)
* Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds
* Daily: 250,000 | 500,000 | 1,000,000
*
* Official documentation states: The search endpoints are rate limited to five requests per second.
* Since with 5 RPS were still hitting secondly rate limits we lowered it to 4
*/
public function getPaginatedData(array $payload, string $type, int $offset = 0): array
{
$total = 0;
$lastId = null;
$rows = [];
foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {
$rows[] = $row;
}
return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
return $this->paginationService->getPaginatedDataGenerator(
$this,
$payload,
$type,
$offset,
$total,
$lastRecordId
);
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
$deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
));
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $batchConfig['api']->read($batchReadRequest);
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
$response = $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
$response = $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
$max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // "110"
$remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // "109"
$interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // "10000"
$body = json_decode((string) $response->getBody(), true);
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));
return $response;
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function createMeeting(array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings';
return $this->makeRequest($endpoint, 'POST', $payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function updateMeeting(string $meetingId, array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings/' . $meetingId;
return $this->makeRequest($endpoint, 'PATCH', $payload);
}
/**
* @throws \Exception
*/
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string {
try {
$noteInput = new SimplePublicObjectInput([
'properties' => [
'hs_note_body' => $body,
'hubspot_owner_id' => $ownerId,
'hs_timestamp' => $timestamp,
],
]);
// Create note
$note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);
$this->getNewInstance()->crm()->objects()->associationsApi()->create(
'note',
$note->getId(),
$this->getNoteObject($noteObject),
$objectId,
$this->getNoteAssociationType($noteObject),
);
return $note->getId();
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to create note', [
'objectId' => $objectId,
'noteObject' => $noteObject->getObjectType(),
'reason' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return null;
}
public function updateEngagement(string $objectId, array $engagement, array $metadata): void
{
$this->getInstance()->engagements()->update($objectId, $engagement, $metadata);
}
public function getEngagementData(string $engagementId): array
{
$engagement = $this->getInstance()->engagements()->get($engagementId);
return $engagement->toArray();
}
public function createEngagement(array $engagement, array $associations, array $metadata): Response
{
return $this->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
}
public function isUnauthorizedException(\Exception $e): bool
{
// Check for specific HubSpot API exception types first
if ($e instanceof BadRequest) {
// BadRequest can contain 401 status codes
return $e->getCode() === 401;
}
// Check for HTTP client exceptions with status codes
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$response = $e->getResponse();
if ($response !== null) {
return $response->getStatusCode() === 401;
}
}
// Check for Guzzle HTTP exceptions
if ($e instanceof \GuzzleHttp\Exception\ClientException) {
return $e->getCode() === 401;
}
// Fallback to string matching as last resort, but be more specific
$message = strtolower($e->getMessage());
return str_contains($message, '401 unauthorized') ||
str_contains($message, 'http 401') ||
str_contains($message, 'status code 401') ||
(preg_match('/\b401\b/', $message) && str_contains($message, 'unauthorized'));
}
/**
* Validates and refreshes the access token if needed before API requests.
* This ensures long-running processes don't fail due to token expiration.
*
* @throws SocialAccountTokenInvalidException
*/
public function ensureValidToken(): void
{
if ($this->oauthAccount === null) {
return;
}
$newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);
if ($newToken !== null) {
$this->accessToken = $newToken;
}
}
public function getConfig()
{
return $this->config;
}
// returns only active (archived=false)
public function getOwners(): array
{
return $this->getNewInstance()->crm()->owners()->getAll();
}
/**
* @param bool $archived
*
* @return array<Owner>|[]
*/
public function getOwnersArchived(bool $archived = true): array
{
$endpoint = '/crm/v3/owners';
$queryParams = [
'archived' => $archived ? 'true' : 'false',
];
$queryString = http_build_query($queryParams);
$owners = [];
try {
$response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);
$responseData = $response?->toArray();
foreach ($responseData['results'] as $result) {
try {
$owners[] = Owner::create($result);
} catch (Throwable $e) {
$this->log->error('[HubSpot] Failed to process owner data', [
'result' => $result,
'error' => $e->getMessage(),
]);
continue;
}
}
} catch (Throwable $e) {
$this->log->error('HubSpot] Failed to fetch owners', [
'archived' => $archived,
'error' => $e->getMessage(),
]);
return [];
}
return $owners;
}
public function getMeeting(string $engagementId): ObjectWithAssociations
{
return $this->getNewInstance()->crm()->objects()->basicApi()
->getById('meeting', $engagementId, null, 'contact,company,deal');
}
public function deleteEngagement(string $engagementId): void
{
$this->getInstance()->engagements()->delete((int) $engagementId);
}
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array
{
$associationData = [];
$idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);
foreach ($idChunks as $idChunk) {
try {
$batchInput = new \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId();
$batchInput->setInputs(array_map(function ($id) {
$publicObjectId = new \HubSpot\Client\Crm\Associations\Model\PublicObjectId();
$publicObjectId->setId($id);
return $publicObjectId;
}, $idChunk));
$associatedObjectsData = $this
->getNewInstance()
->crm()
->associations()
->batchApi()
->read($fromObject, $toObject, $batchInput);
if ($associatedObjectsData instanceof \HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti) {
foreach ($associatedObjectsData->getResults() as $association) {
$from = $association->getFrom()->getId();
$toAssociations = $association->getTo();
if (! empty($toAssociations)) {
$associationData[$from] = array_map(function ($item) {
return $item->getId();
}, $toAssociations);
}
}
}
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to fetch associations', [
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => $e->getMessage(),
]);
}
}
return $associationData;
}
/**
* @throws \Exception
*/
private function getNoteAssociationType(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'note_to_deal',
NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it
NoteObject::Account => 'note_to_company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
/**
* @throws \Exception
*/
private function getNoteObject(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'deal',
NoteObject::Lead, NoteObject::Contact => 'contact',
NoteObject::Account => 'company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
public function addAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/create";
return $this->makeRequest($endpoint, 'POST', $payload);
}
public function removeAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/archive";
return $this->makeRequest($endpoint, 'POST', $payload);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
2878
|
115
|
2
|
2026-05-07T11:45:35.625323+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778154335625_m1.jpg...
|
PhpStorm
|
faVsco.js – Hubspot/Client.php
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
Editor for custom.log
Sync Changes
Hide This Notification
Code changed:
Hide
2
60
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot;
use HubSpot\Client\Crm\Deals\ApiException as DealApiException;
use HubSpot\Client\Crm\Contacts\ApiException as ContactApiException;
use HubSpot\Client\Crm\Companies\ApiException as CompanyApiException;
use HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations as ContactsWithAssociations;
use HubSpot\Client\Crm\Companies\Model\SimplePublicObjectWithAssociations as CompaniesWithAssociations;
use HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations as DealWithAssociations;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectInput;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectWithAssociations as ObjectWithAssociations;
use HubSpot\Client\Crm\Pipelines\Model\Error;
use HubSpot\Client\Crm\Pipelines\Model\PipelineStage;
use HubSpot\Client\Crm\Properties\Model\Property;
use HubSpot\Discovery\Discovery;
use Jiminny\Component\Utility\Service\ProviderRateLimiter;
use Jiminny\Exceptions\CrmException;
use Jiminny\Exceptions\RateLimitException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Jobs\Crm\NoteObject;
use Jiminny\Models\Crm\Field;
use Jiminny\Services\Crm\BaseClient;
use Jiminny\Services\Crm\Hubspot\DTO\Response\Owner;
use Jiminny\Services\SocialAccountService;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use SevenShores\Hubspot\Factory;
use SevenShores\Hubspot\Http\Response;
use Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService;
use Throwable;
/**
* @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
* @param callable(): T $apiCall
* @return T
*
* @throws RateLimitException
*/
private function executeRequest(callable $apiCall)
{
if (! $this->rateLimiter->canMakeRequest($this->config)) {
$retryAfter = $this->rateLimiter->requestAvailableIn($this->config);
$this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
]);
throw new RateLimitException(
'Hubspot rate limit reached for configuration ' . $this->config->getId(),
$retryAfter,
);
}
$this->rateLimiter->incrementRequestCount($this->config);
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function isHubspotRateLimit(Throwable $e): bool
{
return method_exists($e, 'getCode') && (int) $e->getCode() === 429;
}
private function parseRetryAfter(Throwable $e): int
{
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
return 10;
}
public function getMinimumApiVersion(): string
{
return self::MIN_API_VERSION;
}
public function getInstance(): Factory
{
return new Factory([
'key' => $this->accessToken,
'oauth2' => true,
'base_url' => $this->baseUrl,
]);
}
public function getNewInstance(): Discovery
{
return \HubSpot\Factory::createWithAccessToken($this->accessToken);
}
/**
* Secondly and daily limits for Hubspot API
*
* Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)
* Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds
* Daily: 250,000 | 500,000 | 1,000,000
*
* Official documentation states: The search endpoints are rate limited to five requests per second.
* Since with 5 RPS were still hitting secondly rate limits we lowered it to 4
*/
public function getPaginatedData(array $payload, string $type, int $offset = 0): array
{
$total = 0;
$lastId = null;
$rows = [];
foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {
$rows[] = $row;
}
return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
return $this->paginationService->getPaginatedDataGenerator(
$this,
$payload,
$type,
$offset,
$total,
$lastRecordId
);
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
$deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
));
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $batchConfig['api']->read($batchReadRequest);
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
$response = $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
$response = $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
$max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // "110"
$remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // "109"
$interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // "10000"
$body = json_decode((string) $response->getBody(), true);
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));
return $response;
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function createMeeting(array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings';
return $this->makeRequest($endpoint, 'POST', $payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function updateMeeting(string $meetingId, array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings/' . $meetingId;
return $this->makeRequest($endpoint, 'PATCH', $payload);
}
/**
* @throws \Exception
*/
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string {
try {
$noteInput = new SimplePublicObjectInput([
'properties' => [
'hs_note_body' => $body,
'hubspot_owner_id' => $ownerId,
'hs_timestamp' => $timestamp,
],
]);
// Create note
$note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);
$this->getNewInstance()->crm()->objects()->associationsApi()->create(
'note',
$note->getId(),
$this->getNoteObject($noteObject),
$objectId,
$this->getNoteAssociationType($noteObject),
);
return $note->getId();
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to create note', [
'objectId' => $objectId,
'noteObject' => $noteObject->getObjectType(),
'reason' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return null;
}
public function updateEngagement(string $objectId, array $engagement, array $metadata): void
{
$this->getInstance()->engagements()->update($objectId, $engagement, $metadata);
}
public function getEngagementData(string $engagementId): array
{
$engagement = $this->getInstance()->engagements()->get($engagementId);
return $engagement->toArray();
}
public function createEngagement(array $engagement, array $associations, array $metadata): Response
{
return $this->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
}
public function isUnauthorizedException(\Exception $e): bool
{
// Check for specific HubSpot API exception types first
if ($e instanceof BadRequest) {
// BadRequest can contain 401 status codes
return $e->getCode() === 401;
}
// Check for HTTP client exceptions with status codes
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$response = $e->getResponse();
if ($response !== null) {
return $response->getStatusCode() === 401;
}
}
// Check for Guzzle HTTP exceptions
if ($e instanceof \GuzzleHttp\Exception\ClientException) {
return $e->getCode() === 401;
}
// Fallback to string matching as last resort, but be more specific
$message = strtolower($e->getMessage());
return str_contains($message, '401 unauthorized') ||
str_contains($message, 'http 401') ||
str_contains($message, 'status code 401') ||
(preg_match('/\b401\b/', $message) && str_contains($message, 'unauthorized'));
}
/**
* Validates and refreshes the access token if needed before API requests.
* This ensures long-running processes don't fail due to token expiration.
*
* @throws SocialAccountTokenInvalidException
*/
public function ensureValidToken(): void
{
if ($this->oauthAccount === null) {
return;
}
$newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);
if ($newToken !== null) {
$this->accessToken = $newToken;
}
}
public function getConfig()
{
return $this->config;
}
// returns only active (archived=false)
public function getOwners(): array
{
return $this->getNewInstance()->crm()->owners()->getAll();
}
/**
* @param bool $archived
*
* @return array<Owner>|[]
*/
public function getOwnersArchived(bool $archived = true): array
{
$endpoint = '/crm/v3/owners';
$queryParams = [
'archived' => $archived ? 'true' : 'false',
];
$queryString = http_build_query($queryParams);
$owners = [];
try {
$response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);
$responseData = $response?->toArray();
foreach ($responseData['results'] as $result) {
try {
$owners[] = Owner::create($result);
} catch (Throwable $e) {
$this->log->error('[HubSpot] Failed to process owner data', [
'result' => $result,
'error' => $e->getMessage(),
]);
continue;
}
}
} catch (Throwable $e) {
$this->log->error('HubSpot] Failed to fetch owners', [
'archived' => $archived,
'error' => $e->getMessage(),
]);
return [];
}
return $owners;
}
public function getMeeting(string $engagementId): ObjectWithAssociations
{
return $this->getNewInstance()->crm()->objects()->basicApi()
->getById('meeting', $engagementId, null, 'contact,company,deal');
}
public function deleteEngagement(string $engagementId): void
{
$this->getInstance()->engagements()->delete((int) $engagementId);
}
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array
{
$associationData = [];
$idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);
foreach ($idChunks as $idChunk) {
try {
$batchInput = new \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId();
$batchInput->setInputs(array_map(function ($id) {
$publicObjectId = new \HubSpot\Client\Crm\Associations\Model\PublicObjectId();
$publicObjectId->setId($id);
return $publicObjectId;
}, $idChunk));
$associatedObjectsData = $this
->getNewInstance()
->crm()
->associations()
->batchApi()
->read($fromObject, $toObject, $batchInput);
if ($associatedObjectsData instanceof \HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti) {
foreach ($associatedObjectsData->getResults() as $association) {
$from = $association->getFrom()->getId();
$toAssociations = $association->getTo();
if (! empty($toAssociations)) {
$associationData[$from] = array_map(function ($item) {
return $item->getId();
}, $toAssociations);
}
}
}
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to fetch associations', [
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => $e->getMessage(),
]);
}
}
return $associationData;
}
/**
* @throws \Exception
*/
private function getNoteAssociationType(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'note_to_deal',
NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it
NoteObject::Account => 'note_to_company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
/**
* @throws \Exception
*/
private function getNoteObject(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'deal',
NoteObject::Lead, NoteObject::Contact => 'contact',
NoteObject::Account => 'company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
public function addAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/create";
return $this->makeRequest($endpoint, 'POST', $payload);
}
public function removeAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/archive";
return $this->makeRequest($endpoint, 'POST', $payload);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"on_screen":true,"help_text":"Git Branch: master","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"Editor for custom.log","depth":4,"on_screen":true,"role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.016666668,"height":0.02111111},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"60","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.021527778,"height":0.02111111},"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.015277778,"height":0.025555555},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.014583333,"height":0.025555555},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Component\\Utility\\Service\\ProviderRateLimiter;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n private ProviderRateLimiter $rateLimiter;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager,\n ProviderRateLimiter $rateLimiter,\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n $this->rateLimiter = $rateLimiter;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Single entry point for every HubSpot API call. Enforces the per-portal\n * rate limit configured in the rate_limits table (morphed to the current\n * Configuration) and reacts to a real 429 from HubSpot by translating it\n * into a RateLimitException carrying Retry-After.\n *\n * Wrap any outbound HubSpot call (SDK or raw HTTP) like:\n *\n * $this->executeRequest(fn () => $this->getNewInstance()->crm()->...);\n *\n * @template T\n * @param callable(): T $apiCall\n * @return T\n *\n * @throws RateLimitException\n */\n private function executeRequest(callable $apiCall)\n {\n if (! $this->rateLimiter->canMakeRequest($this->config)) {\n $retryAfter = $this->rateLimiter->requestAvailableIn($this->config);\n\n $this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n ]);\n\n throw new RateLimitException(\n 'Hubspot rate limit reached for configuration ' . $this->config->getId(),\n $retryAfter,\n );\n }\n\n $this->rateLimiter->incrementRequestCount($this->config);\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n private function isHubspotRateLimit(Throwable $e): bool\n {\n return method_exists($e, 'getCode') && (int) $e->getCode() === 429;\n }\n\n private function parseRetryAfter(Throwable $e): int\n {\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n return 10;\n }\n\n public function getMinimumApiVersion(): string\n {\n return self::MIN_API_VERSION;\n }\n\n public function getInstance(): Factory\n {\n return new Factory([\n 'key' => $this->accessToken,\n 'oauth2' => true,\n 'base_url' => $this->baseUrl,\n ]);\n }\n\n public function getNewInstance(): Discovery\n {\n return \\HubSpot\\Factory::createWithAccessToken($this->accessToken);\n }\n\n /**\n * Secondly and daily limits for Hubspot API\n *\n * Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)\n * Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds\n * Daily: 250,000 | 500,000 | 1,000,000\n *\n * Official documentation states: The search endpoints are rate limited to five requests per second.\n * Since with 5 RPS were still hitting secondly rate limits we lowered it to 4\n */\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array\n {\n $total = 0;\n $lastId = null;\n $rows = [];\n foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {\n $rows[] = $row;\n }\n\n return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n return $this->paginationService->getPaginatedDataGenerator(\n $this,\n $payload,\n $type,\n $offset,\n $total,\n $lastRecordId\n );\n }\n\n /**\n * @throws DealApiException\n * @throws CrmException\n */\n public function getOpportunityById(string $crmId, array $fields): array\n {\n try {\n $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n ));\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n $response = $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n $response = $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n\n $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // \"110\"\n $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // \"109\"\n $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // \"10000\"\n $body = json_decode((string) $response->getBody(), true);\n\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));\n\n return $response;\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Component\\Utility\\Service\\ProviderRateLimiter;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n private ProviderRateLimiter $rateLimiter;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager,\n ProviderRateLimiter $rateLimiter,\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n $this->rateLimiter = $rateLimiter;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Single entry point for every HubSpot API call. Enforces the per-portal\n * rate limit configured in the rate_limits table (morphed to the current\n * Configuration) and reacts to a real 429 from HubSpot by translating it\n * into a RateLimitException carrying Retry-After.\n *\n * Wrap any outbound HubSpot call (SDK or raw HTTP) like:\n *\n * $this->executeRequest(fn () => $this->getNewInstance()->crm()->...);\n *\n * @template T\n * @param callable(): T $apiCall\n * @return T\n *\n * @throws RateLimitException\n */\n private function executeRequest(callable $apiCall)\n {\n if (! $this->rateLimiter->canMakeRequest($this->config)) {\n $retryAfter = $this->rateLimiter->requestAvailableIn($this->config);\n\n $this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n ]);\n\n throw new RateLimitException(\n 'Hubspot rate limit reached for configuration ' . $this->config->getId(),\n $retryAfter,\n );\n }\n\n $this->rateLimiter->incrementRequestCount($this->config);\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n private function isHubspotRateLimit(Throwable $e): bool\n {\n return method_exists($e, 'getCode') && (int) $e->getCode() === 429;\n }\n\n private function parseRetryAfter(Throwable $e): int\n {\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n return 10;\n }\n\n public function getMinimumApiVersion(): string\n {\n return self::MIN_API_VERSION;\n }\n\n public function getInstance(): Factory\n {\n return new Factory([\n 'key' => $this->accessToken,\n 'oauth2' => true,\n 'base_url' => $this->baseUrl,\n ]);\n }\n\n public function getNewInstance(): Discovery\n {\n return \\HubSpot\\Factory::createWithAccessToken($this->accessToken);\n }\n\n /**\n * Secondly and daily limits for Hubspot API\n *\n * Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)\n * Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds\n * Daily: 250,000 | 500,000 | 1,000,000\n *\n * Official documentation states: The search endpoints are rate limited to five requests per second.\n * Since with 5 RPS were still hitting secondly rate limits we lowered it to 4\n */\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array\n {\n $total = 0;\n $lastId = null;\n $rows = [];\n foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {\n $rows[] = $row;\n }\n\n return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n return $this->paginationService->getPaginatedDataGenerator(\n $this,\n $payload,\n $type,\n $offset,\n $total,\n $lastRecordId\n );\n }\n\n /**\n * @throws DealApiException\n * @throws CrmException\n */\n public function getOpportunityById(string $crmId, array $fields): array\n {\n try {\n $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n ));\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n $response = $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n $response = $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n\n $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // \"110\"\n $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // \"109\"\n $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // \"10000\"\n $body = json_decode((string) $response->getBody(), true);\n\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));\n\n return $response;\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-188086479456983350
|
6378757184196315236
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
Editor for custom.log
Sync Changes
Hide This Notification
Code changed:
Hide
2
60
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot;
use HubSpot\Client\Crm\Deals\ApiException as DealApiException;
use HubSpot\Client\Crm\Contacts\ApiException as ContactApiException;
use HubSpot\Client\Crm\Companies\ApiException as CompanyApiException;
use HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations as ContactsWithAssociations;
use HubSpot\Client\Crm\Companies\Model\SimplePublicObjectWithAssociations as CompaniesWithAssociations;
use HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations as DealWithAssociations;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectInput;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectWithAssociations as ObjectWithAssociations;
use HubSpot\Client\Crm\Pipelines\Model\Error;
use HubSpot\Client\Crm\Pipelines\Model\PipelineStage;
use HubSpot\Client\Crm\Properties\Model\Property;
use HubSpot\Discovery\Discovery;
use Jiminny\Component\Utility\Service\ProviderRateLimiter;
use Jiminny\Exceptions\CrmException;
use Jiminny\Exceptions\RateLimitException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Jobs\Crm\NoteObject;
use Jiminny\Models\Crm\Field;
use Jiminny\Services\Crm\BaseClient;
use Jiminny\Services\Crm\Hubspot\DTO\Response\Owner;
use Jiminny\Services\SocialAccountService;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use SevenShores\Hubspot\Factory;
use SevenShores\Hubspot\Http\Response;
use Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService;
use Throwable;
/**
* @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
* @param callable(): T $apiCall
* @return T
*
* @throws RateLimitException
*/
private function executeRequest(callable $apiCall)
{
if (! $this->rateLimiter->canMakeRequest($this->config)) {
$retryAfter = $this->rateLimiter->requestAvailableIn($this->config);
$this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
]);
throw new RateLimitException(
'Hubspot rate limit reached for configuration ' . $this->config->getId(),
$retryAfter,
);
}
$this->rateLimiter->incrementRequestCount($this->config);
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function isHubspotRateLimit(Throwable $e): bool
{
return method_exists($e, 'getCode') && (int) $e->getCode() === 429;
}
private function parseRetryAfter(Throwable $e): int
{
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
return 10;
}
public function getMinimumApiVersion(): string
{
return self::MIN_API_VERSION;
}
public function getInstance(): Factory
{
return new Factory([
'key' => $this->accessToken,
'oauth2' => true,
'base_url' => $this->baseUrl,
]);
}
public function getNewInstance(): Discovery
{
return \HubSpot\Factory::createWithAccessToken($this->accessToken);
}
/**
* Secondly and daily limits for Hubspot API
*
* Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)
* Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds
* Daily: 250,000 | 500,000 | 1,000,000
*
* Official documentation states: The search endpoints are rate limited to five requests per second.
* Since with 5 RPS were still hitting secondly rate limits we lowered it to 4
*/
public function getPaginatedData(array $payload, string $type, int $offset = 0): array
{
$total = 0;
$lastId = null;
$rows = [];
foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {
$rows[] = $row;
}
return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
return $this->paginationService->getPaginatedDataGenerator(
$this,
$payload,
$type,
$offset,
$total,
$lastRecordId
);
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
$deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
));
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $batchConfig['api']->read($batchReadRequest);
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
$response = $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
$response = $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
$max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // "110"
$remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // "109"
$interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // "10000"
$body = json_decode((string) $response->getBody(), true);
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));
return $response;
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function createMeeting(array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings';
return $this->makeRequest($endpoint, 'POST', $payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function updateMeeting(string $meetingId, array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings/' . $meetingId;
return $this->makeRequest($endpoint, 'PATCH', $payload);
}
/**
* @throws \Exception
*/
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string {
try {
$noteInput = new SimplePublicObjectInput([
'properties' => [
'hs_note_body' => $body,
'hubspot_owner_id' => $ownerId,
'hs_timestamp' => $timestamp,
],
]);
// Create note
$note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);
$this->getNewInstance()->crm()->objects()->associationsApi()->create(
'note',
$note->getId(),
$this->getNoteObject($noteObject),
$objectId,
$this->getNoteAssociationType($noteObject),
);
return $note->getId();
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to create note', [
'objectId' => $objectId,
'noteObject' => $noteObject->getObjectType(),
'reason' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return null;
}
public function updateEngagement(string $objectId, array $engagement, array $metadata): void
{
$this->getInstance()->engagements()->update($objectId, $engagement, $metadata);
}
public function getEngagementData(string $engagementId): array
{
$engagement = $this->getInstance()->engagements()->get($engagementId);
return $engagement->toArray();
}
public function createEngagement(array $engagement, array $associations, array $metadata): Response
{
return $this->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
}
public function isUnauthorizedException(\Exception $e): bool
{
// Check for specific HubSpot API exception types first
if ($e instanceof BadRequest) {
// BadRequest can contain 401 status codes
return $e->getCode() === 401;
}
// Check for HTTP client exceptions with status codes
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$response = $e->getResponse();
if ($response !== null) {
return $response->getStatusCode() === 401;
}
}
// Check for Guzzle HTTP exceptions
if ($e instanceof \GuzzleHttp\Exception\ClientException) {
return $e->getCode() === 401;
}
// Fallback to string matching as last resort, but be more specific
$message = strtolower($e->getMessage());
return str_contains($message, '401 unauthorized') ||
str_contains($message, 'http 401') ||
str_contains($message, 'status code 401') ||
(preg_match('/\b401\b/', $message) && str_contains($message, 'unauthorized'));
}
/**
* Validates and refreshes the access token if needed before API requests.
* This ensures long-running processes don't fail due to token expiration.
*
* @throws SocialAccountTokenInvalidException
*/
public function ensureValidToken(): void
{
if ($this->oauthAccount === null) {
return;
}
$newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);
if ($newToken !== null) {
$this->accessToken = $newToken;
}
}
public function getConfig()
{
return $this->config;
}
// returns only active (archived=false)
public function getOwners(): array
{
return $this->getNewInstance()->crm()->owners()->getAll();
}
/**
* @param bool $archived
*
* @return array<Owner>|[]
*/
public function getOwnersArchived(bool $archived = true): array
{
$endpoint = '/crm/v3/owners';
$queryParams = [
'archived' => $archived ? 'true' : 'false',
];
$queryString = http_build_query($queryParams);
$owners = [];
try {
$response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);
$responseData = $response?->toArray();
foreach ($responseData['results'] as $result) {
try {
$owners[] = Owner::create($result);
} catch (Throwable $e) {
$this->log->error('[HubSpot] Failed to process owner data', [
'result' => $result,
'error' => $e->getMessage(),
]);
continue;
}
}
} catch (Throwable $e) {
$this->log->error('HubSpot] Failed to fetch owners', [
'archived' => $archived,
'error' => $e->getMessage(),
]);
return [];
}
return $owners;
}
public function getMeeting(string $engagementId): ObjectWithAssociations
{
return $this->getNewInstance()->crm()->objects()->basicApi()
->getById('meeting', $engagementId, null, 'contact,company,deal');
}
public function deleteEngagement(string $engagementId): void
{
$this->getInstance()->engagements()->delete((int) $engagementId);
}
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array
{
$associationData = [];
$idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);
foreach ($idChunks as $idChunk) {
try {
$batchInput = new \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId();
$batchInput->setInputs(array_map(function ($id) {
$publicObjectId = new \HubSpot\Client\Crm\Associations\Model\PublicObjectId();
$publicObjectId->setId($id);
return $publicObjectId;
}, $idChunk));
$associatedObjectsData = $this
->getNewInstance()
->crm()
->associations()
->batchApi()
->read($fromObject, $toObject, $batchInput);
if ($associatedObjectsData instanceof \HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti) {
foreach ($associatedObjectsData->getResults() as $association) {
$from = $association->getFrom()->getId();
$toAssociations = $association->getTo();
if (! empty($toAssociations)) {
$associationData[$from] = array_map(function ($item) {
return $item->getId();
}, $toAssociations);
}
}
}
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to fetch associations', [
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => $e->getMessage(),
]);
}
}
return $associationData;
}
/**
* @throws \Exception
*/
private function getNoteAssociationType(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'note_to_deal',
NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it
NoteObject::Account => 'note_to_company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
/**
* @throws \Exception
*/
private function getNoteObject(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'deal',
NoteObject::Lead, NoteObject::Contact => 'contact',
NoteObject::Account => 'company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
public function addAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/create";
return $this->makeRequest($endpoint, 'POST', $payload);
}
public function removeAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/archive";
return $this->makeRequest($endpoint, 'POST', $payload);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
2877
|
116
|
3
|
2026-05-07T11:45:33.324628+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778154333324_m2.jpg...
|
PhpStorm
|
faVsco.js – Hubspot/Client.php
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
PhostormINavigarecodeLaravelKeractorFV faVsco.js°9 PhostormINavigarecodeLaravelKeractorFV faVsco.js°9 masterProiect vRematchActivityOnCrmObjectDetach.phpT DeleteCrmEntityTrait.php© BatchSyncCollector., RateLimitException.pnpe balchsynckealsservAddkateLimitcommand.pnp© Hubspot/Client.php X © SyncOpportunity.php© WebhookSyncBatchProcessor.phpc clientonpc closeaDealstagesseDealFieldsService.phHitp/RateLimited.png© BaseRateLimiter.phpC) service.phgT SyncCrmEntitiesTrait.php© SyncTeamMetadata.phpuRateLimitintenace.oho© RateLimiterInstance.phpc)DecorateAcuiviv.onc© FieldDefinitions.phpclass Cllent extends Baseclient 1mpLements Hubspotclientintertacec) FieldTvpeconverter.@ HubspotClientInterfa(c) HubspotTokenMana(C) PavloadBuilder.php(C) RemotecrmObiectM.@ ResponseNormalize.c) Service.ohr© SyncFieldAction.php(C) SvncRelatedActivitvi@ WebhookSvncBatch!v MintearationAor>IM Acceccors>D Config> DDTO> D Filters> D Jobs› D ProspectSearchStratm Servicetraits(e) DataClient.php© DecorateActivity.php© LocalSearch.php© LocalSearchInterface© RemoteSearch.php(c) Service.phpv D Listeners101(c) ConvertLeadActivitiec) PurceLookuocache.rI1Az1> M Metadata> D MiarationiM Pipedrivev Salesforce1061107… D Fields• M OnnortunitvMatchen109M OnnortunitvSvneStral> M ProsnectSearchStrat• M ServiceTraits(C) Client nhn© DecorateActivity.phpT DeleteObjectsTrait.p© FieldDefinitions.php© PayloadBuilder.php© Profile.php© QueryBuilder.php114116117LuSagte funation executeRequest(co11ab1e kantCo1))1r sthis->ratelimiter->canMakeRequeststhis->conf10002SretryAfter = $this->rateLimiter->requestAva1lableln(sthis->conf1g):$this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [= Sthis->confio->team 1diconfig_id=> $this->config-›getidO.thnow new Ratel imitFycentiondmessage: 'Hubspot rate limit reached for configuration'. $this->config->getIdOlSretrvAfter,Sthis->rateLimiter->incrementRequestCount($this->config):tryfreturn SapiCallo:} catch (Throwable $e) {if ($this->isHubspotRateLimit($e)) {SretrvAtter = Sth1s->parseretrvArterSe)*Sthis->loa->warnina('[Hubspotl Received 429 from APT'.. [lconfia id'= Sthis->confio->aetido=> Se->aetMessageOlD):+hrow new Ratel imi+Fycention messace: "Huhsnot returned 420'. SretrvAften. So)•throw $e;T 7 of 8 editsAccept File &++X Reject File 0%€+ 2 0f 4 files →arAuhe foa ids suadestiionsa Detect more secwritvlissues lin vour D!Dfflles //lTin SonarAube Cloud for free //lDownload SonarOmbe Server Illear more //lDonit ask adain /itodav 105251S0 lib o# Support Daily - in 15 m100% C• Thu 7 May 14:45:33AskJiminnyReportActivityServiceTest v= custom.log X = laravel.log« SF [jiminny@localhost]A HS_local [jiminny@localhost]# console [PKol)A console (eu)« console [STAGING]io 4 spaces ©...
|
NULL
|
-2258402779621802861
|
NULL
|
click
|
ocr
|
NULL
|
PhostormINavigarecodeLaravelKeractorFV faVsco.js°9 PhostormINavigarecodeLaravelKeractorFV faVsco.js°9 masterProiect vRematchActivityOnCrmObjectDetach.phpT DeleteCrmEntityTrait.php© BatchSyncCollector., RateLimitException.pnpe balchsynckealsservAddkateLimitcommand.pnp© Hubspot/Client.php X © SyncOpportunity.php© WebhookSyncBatchProcessor.phpc clientonpc closeaDealstagesseDealFieldsService.phHitp/RateLimited.png© BaseRateLimiter.phpC) service.phgT SyncCrmEntitiesTrait.php© SyncTeamMetadata.phpuRateLimitintenace.oho© RateLimiterInstance.phpc)DecorateAcuiviv.onc© FieldDefinitions.phpclass Cllent extends Baseclient 1mpLements Hubspotclientintertacec) FieldTvpeconverter.@ HubspotClientInterfa(c) HubspotTokenMana(C) PavloadBuilder.php(C) RemotecrmObiectM.@ ResponseNormalize.c) Service.ohr© SyncFieldAction.php(C) SvncRelatedActivitvi@ WebhookSvncBatch!v MintearationAor>IM Acceccors>D Config> DDTO> D Filters> D Jobs› D ProspectSearchStratm Servicetraits(e) DataClient.php© DecorateActivity.php© LocalSearch.php© LocalSearchInterface© RemoteSearch.php(c) Service.phpv D Listeners101(c) ConvertLeadActivitiec) PurceLookuocache.rI1Az1> M Metadata> D MiarationiM Pipedrivev Salesforce1061107… D Fields• M OnnortunitvMatchen109M OnnortunitvSvneStral> M ProsnectSearchStrat• M ServiceTraits(C) Client nhn© DecorateActivity.phpT DeleteObjectsTrait.p© FieldDefinitions.php© PayloadBuilder.php© Profile.php© QueryBuilder.php114116117LuSagte funation executeRequest(co11ab1e kantCo1))1r sthis->ratelimiter->canMakeRequeststhis->conf10002SretryAfter = $this->rateLimiter->requestAva1lableln(sthis->conf1g):$this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [= Sthis->confio->team 1diconfig_id=> $this->config-›getidO.thnow new Ratel imitFycentiondmessage: 'Hubspot rate limit reached for configuration'. $this->config->getIdOlSretrvAfter,Sthis->rateLimiter->incrementRequestCount($this->config):tryfreturn SapiCallo:} catch (Throwable $e) {if ($this->isHubspotRateLimit($e)) {SretrvAtter = Sth1s->parseretrvArterSe)*Sthis->loa->warnina('[Hubspotl Received 429 from APT'.. [lconfia id'= Sthis->confio->aetido=> Se->aetMessageOlD):+hrow new Ratel imi+Fycention messace: "Huhsnot returned 420'. SretrvAften. So)•throw $e;T 7 of 8 editsAccept File &++X Reject File 0%€+ 2 0f 4 files →arAuhe foa ids suadestiionsa Detect more secwritvlissues lin vour D!Dfflles //lTin SonarAube Cloud for free //lDownload SonarOmbe Server Illear more //lDonit ask adain /itodav 105251S0 lib o# Support Daily - in 15 m100% C• Thu 7 May 14:45:33AskJiminnyReportActivityServiceTest v= custom.log X = laravel.log« SF [jiminny@localhost]A HS_local [jiminny@localhost]# console [PKol)A console (eu)« console [STAGING]io 4 spaces ©...
|
2874
|
NULL
|
NULL
|
NULL
|
|
2876
|
115
|
1
|
2026-05-07T11:45:33.324512+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778154333324_m1.jpg...
|
PhpStorm
|
faVsco.js – Hubspot/Client.php
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
Editor for custom.log
Sync Changes
Hide This Notification
Code changed:
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"on_screen":true,"help_text":"Git Branch: master","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"Editor for custom.log","depth":4,"on_screen":true,"role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-7357289762196789584
|
-8132368178556597310
|
click
|
hybrid
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
Editor for custom.log
Sync Changes
Hide This Notification
Code changed:
Hide
iTerm2ShellEditViewSessionScriptsProfilesWindowHelp$0lihoSupport Daily - in 15 m100% <478DEV (docker)DOCKERDEV (docker)882APP (-zsh)-zshjiminny-worker-processing-4:j1minny-worker-processing-4_00:jiminny-worker-processing-5:jiminny-worker-processing-5_00:stoppedstoppedworker-analytics:worker-analytics_00: stoppedworker-crm-update:worker-crm-update_00: stoppedartisan-schedule:artisan-schedule_00:stoppedworker-nudges:worker-nudges_00: stoppedworker:worker_00: stoppedjiminny-worker-processing-1:jiminny-worker-processing-1_00: stoppedworker-audio:worker-audio_00: stoppedworker-calendar:worker-calendar_00:stoppedworker-conferences:worker-conferences_00:stoppedworker-crm-sync:worker-crm-sync_00:stoppedworker-emails:worker-emails_00:stoppedworker-es-update:worker-es-update_00: stoppedartisan-schedule:artisan-schedule_00:startedjiminny-worker-processing-1:jiminny-worker-processing-1_00: startedjiminny-worker-processing-2:jiminny-worker-processing-2_00: startedjiminny-worker-processing-3:jiminny-worker-processing-3_00: startedjiminny-worker-processing-4:jiminny-worker-processing-4_00: startedjiminny-worker-processing-5:jiminny-worker-processing-5_00: startedjiminny-worker-processing-delayed: jiminny-worker-processing-delayed_00: startedworker:worker_00: startedworker-analytics:worker-analytics_00: startedworker-audio:worker-audio_00: startedworker-calendar:worker-calendar_00: startedworker-conferences:worker-conferences_00: startedworker-crm-sync:worker-crm-sync_00: startedworker-crm-update:worker-crm-update_00: startedworker-download:worker-download_00:startedworker-emails:worker-emails_00: startedworker-es-update:worker-es-update_00: startedworker-nudges:worker-nudges_00: startedroot@docker_lamp_1:/home/jiminny# php artisan crm:sync-opportunity --teamId=2 --from='2026-05-01 00:00:00'Syncing opportunity for HubspotSyncing opportunities modified since 2026-05-01 00:00:00...Synced 6 opportunities.root@docker_lamp_1:/home/jiminny# php artisan crm:sync-opportunity --teamId=2 --opportunityId 374720564Syncing opportunity for HubspotSyncing opportunity 374720564...Synced AmirHSOpp to 5066root@docker_lamp_1:/home/jiminny# php artisan crm: sync-contact --teamId=2 --contactId 21351Syncing contact(s) for HubspotSyncing contact 21351...Synced Lissy Newlandto 464root@docker_lamp_1:/home/jiminny# ]• $4screenpipe"•$5-zshThu 7 May 14:45:33T81₴6DEV...
|
2868
|
NULL
|
NULL
|
NULL
|
|
2875
|
115
|
0
|
2026-05-07T11:44:59.316228+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778154299316_m1.jpg...
|
PhpStorm
|
faVsco.js – Hubspot/Client.php
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
Editor for custom.log
Sync Changes
Hide This Notification
Code changed:
Hide
2
60
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot;
use HubSpot\Client\Crm\Deals\ApiException as DealApiException;
use HubSpot\Client\Crm\Contacts\ApiException as ContactApiException;
use HubSpot\Client\Crm\Companies\ApiException as CompanyApiException;
use HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations as ContactsWithAssociations;
use HubSpot\Client\Crm\Companies\Model\SimplePublicObjectWithAssociations as CompaniesWithAssociations;
use HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations as DealWithAssociations;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectInput;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectWithAssociations as ObjectWithAssociations;
use HubSpot\Client\Crm\Pipelines\Model\Error;
use HubSpot\Client\Crm\Pipelines\Model\PipelineStage;
use HubSpot\Client\Crm\Properties\Model\Property;
use HubSpot\Discovery\Discovery;
use Jiminny\Component\Utility\Service\ProviderRateLimiter;
use Jiminny\Exceptions\CrmException;
use Jiminny\Exceptions\RateLimitException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Jobs\Crm\NoteObject;
use Jiminny\Models\Crm\Field;
use Jiminny\Services\Crm\BaseClient;
use Jiminny\Services\Crm\Hubspot\DTO\Response\Owner;
use Jiminny\Services\SocialAccountService;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use SevenShores\Hubspot\Factory;
use SevenShores\Hubspot\Http\Response;
use Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService;
use Throwable;
/**
* @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
* @param callable(): T $apiCall
* @return T
*
* @throws RateLimitException
*/
private function executeRequest(callable $apiCall)
{
if (! $this->rateLimiter->canMakeRequest($this->config)) {
$retryAfter = $this->rateLimiter->requestAvailableIn($this->config);
$this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
]);
throw new RateLimitException(
'Hubspot rate limit reached for configuration ' . $this->config->getId(),
$retryAfter,
);
}
$this->rateLimiter->incrementRequestCount($this->config);
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function isHubspotRateLimit(Throwable $e): bool
{
return method_exists($e, 'getCode') && (int) $e->getCode() === 429;
}
private function parseRetryAfter(Throwable $e): int
{
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
return 10;
}
public function getMinimumApiVersion(): string
{
return self::MIN_API_VERSION;
}
public function getInstance(): Factory
{
return new Factory([
'key' => $this->accessToken,
'oauth2' => true,
'base_url' => $this->baseUrl,
]);
}
public function getNewInstance(): Discovery
{
return \HubSpot\Factory::createWithAccessToken($this->accessToken);
}
/**
* Secondly and daily limits for Hubspot API
*
* Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)
* Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds
* Daily: 250,000 | 500,000 | 1,000,000
*
* Official documentation states: The search endpoints are rate limited to five requests per second.
* Since with 5 RPS were still hitting secondly rate limits we lowered it to 4
*/
public function getPaginatedData(array $payload, string $type, int $offset = 0): array
{
$total = 0;
$lastId = null;
$rows = [];
foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {
$rows[] = $row;
}
return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
return $this->paginationService->getPaginatedDataGenerator(
$this,
$payload,
$type,
$offset,
$total,
$lastRecordId
);
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
$deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
));
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $batchConfig['api']->read($batchReadRequest);
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
$response = $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
$response = $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
$max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // "110"
$remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // "109"
$interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // "10000"
$body = json_decode((string) $response->getBody(), true);
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));
return $response;
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function createMeeting(array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings';
return $this->makeRequest($endpoint, 'POST', $payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function updateMeeting(string $meetingId, array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings/' . $meetingId;
return $this->makeRequest($endpoint, 'PATCH', $payload);
}
/**
* @throws \Exception
*/
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string {
try {
$noteInput = new SimplePublicObjectInput([
'properties' => [
'hs_note_body' => $body,
'hubspot_owner_id' => $ownerId,
'hs_timestamp' => $timestamp,
],
]);
// Create note
$note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);
$this->getNewInstance()->crm()->objects()->associationsApi()->create(
'note',
$note->getId(),
$this->getNoteObject($noteObject),
$objectId,
$this->getNoteAssociationType($noteObject),
);
return $note->getId();
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to create note', [
'objectId' => $objectId,
'noteObject' => $noteObject->getObjectType(),
'reason' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return null;
}
public function updateEngagement(string $objectId, array $engagement, array $metadata): void
{
$this->getInstance()->engagements()->update($objectId, $engagement, $metadata);
}
public function getEngagementData(string $engagementId): array
{
$engagement = $this->getInstance()->engagements()->get($engagementId);
return $engagement->toArray();
}
public function createEngagement(array $engagement, array $associations, array $metadata): Response
{
return $this->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
}
public function isUnauthorizedException(\Exception $e): bool
{
// Check for specific HubSpot API exception types first
if ($e instanceof BadRequest) {
// BadRequest can contain 401 status codes
return $e->getCode() === 401;
}
// Check for HTTP client exceptions with status codes
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$response = $e->getResponse();
if ($response !== null) {
return $response->getStatusCode() === 401;
}
}
// Check for Guzzle HTTP exceptions
if ($e instanceof \GuzzleHttp\Exception\ClientException) {
return $e->getCode() === 401;
}
// Fallback to string matching as last resort, but be more specific
$message = strtolower($e->getMessage());
return str_contains($message, '401 unauthorized') ||
str_contains($message, 'http 401') ||
str_contains($message, 'status code 401') ||
(preg_match('/\b401\b/', $message) && str_contains($message, 'unauthorized'));
}
/**
* Validates and refreshes the access token if needed before API requests.
* This ensures long-running processes don't fail due to token expiration.
*
* @throws SocialAccountTokenInvalidException
*/
public function ensureValidToken(): void
{
if ($this->oauthAccount === null) {
return;
}
$newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);
if ($newToken !== null) {
$this->accessToken = $newToken;
}
}
public function getConfig()
{
return $this->config;
}
// returns only active (archived=false)
public function getOwners(): array
{
return $this->getNewInstance()->crm()->owners()->getAll();
}
/**
* @param bool $archived
*
* @return array<Owner>|[]
*/
public function getOwnersArchived(bool $archived = true): array
{
$endpoint = '/crm/v3/owners';
$queryParams = [
'archived' => $archived ? 'true' : 'false',
];
$queryString = http_build_query($queryParams);
$owners = [];
try {
$response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);
$responseData = $response?->toArray();
foreach ($responseData['results'] as $result) {
try {
$owners[] = Owner::create($result);
} catch (Throwable $e) {
$this->log->error('[HubSpot] Failed to process owner data', [
'result' => $result,
'error' => $e->getMessage(),
]);
continue;
}
}
} catch (Throwable $e) {
$this->log->error('HubSpot] Failed to fetch owners', [
'archived' => $archived,
'error' => $e->getMessage(),
]);
return [];
}
return $owners;
}
public function getMeeting(string $engagementId): ObjectWithAssociations
{
return $this->getNewInstance()->crm()->objects()->basicApi()
->getById('meeting', $engagementId, null, 'contact,company,deal');
}
public function deleteEngagement(string $engagementId): void
{
$this->getInstance()->engagements()->delete((int) $engagementId);
}
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array
{
$associationData = [];
$idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);
foreach ($idChunks as $idChunk) {
try {
$batchInput = new \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId();
$batchInput->setInputs(array_map(function ($id) {
$publicObjectId = new \HubSpot\Client\Crm\Associations\Model\PublicObjectId();
$publicObjectId->setId($id);
return $publicObjectId;
}, $idChunk));
$associatedObjectsData = $this
->getNewInstance()
->crm()
->associations()
->batchApi()
->read($fromObject, $toObject, $batchInput);
if ($associatedObjectsData instanceof \HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti) {
foreach ($associatedObjectsData->getResults() as $association) {
$from = $association->getFrom()->getId();
$toAssociations = $association->getTo();
if (! empty($toAssociations)) {
$associationData[$from] = array_map(function ($item) {
return $item->getId();
}, $toAssociations);
}
}
}
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to fetch associations', [
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => $e->getMessage(),
]);
}
}
return $associationData;
}
/**
* @throws \Exception
*/
private function getNoteAssociationType(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'note_to_deal',
NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it
NoteObject::Account => 'note_to_company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
/**
* @throws \Exception
*/
private function getNoteObject(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'deal',
NoteObject::Lead, NoteObject::Contact => 'contact',
NoteObject::Account => 'company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
public function addAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/create";
return $this->makeRequest($endpoint, 'POST', $payload);
}
public function removeAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/archive";
return $this->makeRequest($endpoint, 'POST', $payload);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"on_screen":true,"help_text":"Git Branch: master","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"Editor for custom.log","depth":4,"on_screen":true,"role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.016666668,"height":0.02111111},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"60","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.021527778,"height":0.02111111},"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.015277778,"height":0.025555555},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.014583333,"height":0.025555555},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Component\\Utility\\Service\\ProviderRateLimiter;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n private ProviderRateLimiter $rateLimiter;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager,\n ProviderRateLimiter $rateLimiter,\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n $this->rateLimiter = $rateLimiter;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Single entry point for every HubSpot API call. Enforces the per-portal\n * rate limit configured in the rate_limits table (morphed to the current\n * Configuration) and reacts to a real 429 from HubSpot by translating it\n * into a RateLimitException carrying Retry-After.\n *\n * Wrap any outbound HubSpot call (SDK or raw HTTP) like:\n *\n * $this->executeRequest(fn () => $this->getNewInstance()->crm()->...);\n *\n * @template T\n * @param callable(): T $apiCall\n * @return T\n *\n * @throws RateLimitException\n */\n private function executeRequest(callable $apiCall)\n {\n if (! $this->rateLimiter->canMakeRequest($this->config)) {\n $retryAfter = $this->rateLimiter->requestAvailableIn($this->config);\n\n $this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n ]);\n\n throw new RateLimitException(\n 'Hubspot rate limit reached for configuration ' . $this->config->getId(),\n $retryAfter,\n );\n }\n\n $this->rateLimiter->incrementRequestCount($this->config);\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n private function isHubspotRateLimit(Throwable $e): bool\n {\n return method_exists($e, 'getCode') && (int) $e->getCode() === 429;\n }\n\n private function parseRetryAfter(Throwable $e): int\n {\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n return 10;\n }\n\n public function getMinimumApiVersion(): string\n {\n return self::MIN_API_VERSION;\n }\n\n public function getInstance(): Factory\n {\n return new Factory([\n 'key' => $this->accessToken,\n 'oauth2' => true,\n 'base_url' => $this->baseUrl,\n ]);\n }\n\n public function getNewInstance(): Discovery\n {\n return \\HubSpot\\Factory::createWithAccessToken($this->accessToken);\n }\n\n /**\n * Secondly and daily limits for Hubspot API\n *\n * Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)\n * Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds\n * Daily: 250,000 | 500,000 | 1,000,000\n *\n * Official documentation states: The search endpoints are rate limited to five requests per second.\n * Since with 5 RPS were still hitting secondly rate limits we lowered it to 4\n */\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array\n {\n $total = 0;\n $lastId = null;\n $rows = [];\n foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {\n $rows[] = $row;\n }\n\n return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n return $this->paginationService->getPaginatedDataGenerator(\n $this,\n $payload,\n $type,\n $offset,\n $total,\n $lastRecordId\n );\n }\n\n /**\n * @throws DealApiException\n * @throws CrmException\n */\n public function getOpportunityById(string $crmId, array $fields): array\n {\n try {\n $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n ));\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n $response = $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n $response = $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n\n $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // \"110\"\n $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // \"109\"\n $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // \"10000\"\n $body = json_decode((string) $response->getBody(), true);\n\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));\n\n return $response;\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Component\\Utility\\Service\\ProviderRateLimiter;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n private ProviderRateLimiter $rateLimiter;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager,\n ProviderRateLimiter $rateLimiter,\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n $this->rateLimiter = $rateLimiter;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Single entry point for every HubSpot API call. Enforces the per-portal\n * rate limit configured in the rate_limits table (morphed to the current\n * Configuration) and reacts to a real 429 from HubSpot by translating it\n * into a RateLimitException carrying Retry-After.\n *\n * Wrap any outbound HubSpot call (SDK or raw HTTP) like:\n *\n * $this->executeRequest(fn () => $this->getNewInstance()->crm()->...);\n *\n * @template T\n * @param callable(): T $apiCall\n * @return T\n *\n * @throws RateLimitException\n */\n private function executeRequest(callable $apiCall)\n {\n if (! $this->rateLimiter->canMakeRequest($this->config)) {\n $retryAfter = $this->rateLimiter->requestAvailableIn($this->config);\n\n $this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n ]);\n\n throw new RateLimitException(\n 'Hubspot rate limit reached for configuration ' . $this->config->getId(),\n $retryAfter,\n );\n }\n\n $this->rateLimiter->incrementRequestCount($this->config);\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n private function isHubspotRateLimit(Throwable $e): bool\n {\n return method_exists($e, 'getCode') && (int) $e->getCode() === 429;\n }\n\n private function parseRetryAfter(Throwable $e): int\n {\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n return 10;\n }\n\n public function getMinimumApiVersion(): string\n {\n return self::MIN_API_VERSION;\n }\n\n public function getInstance(): Factory\n {\n return new Factory([\n 'key' => $this->accessToken,\n 'oauth2' => true,\n 'base_url' => $this->baseUrl,\n ]);\n }\n\n public function getNewInstance(): Discovery\n {\n return \\HubSpot\\Factory::createWithAccessToken($this->accessToken);\n }\n\n /**\n * Secondly and daily limits for Hubspot API\n *\n * Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)\n * Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds\n * Daily: 250,000 | 500,000 | 1,000,000\n *\n * Official documentation states: The search endpoints are rate limited to five requests per second.\n * Since with 5 RPS were still hitting secondly rate limits we lowered it to 4\n */\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array\n {\n $total = 0;\n $lastId = null;\n $rows = [];\n foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {\n $rows[] = $row;\n }\n\n return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n return $this->paginationService->getPaginatedDataGenerator(\n $this,\n $payload,\n $type,\n $offset,\n $total,\n $lastRecordId\n );\n }\n\n /**\n * @throws DealApiException\n * @throws CrmException\n */\n public function getOpportunityById(string $crmId, array $fields): array\n {\n try {\n $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n ));\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n $response = $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n $response = $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n\n $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // \"110\"\n $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // \"109\"\n $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // \"10000\"\n $body = json_decode((string) $response->getBody(), true);\n\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));\n\n return $response;\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-188086479456983350
|
6378757184196315236
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
Editor for custom.log
Sync Changes
Hide This Notification
Code changed:
Hide
2
60
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot;
use HubSpot\Client\Crm\Deals\ApiException as DealApiException;
use HubSpot\Client\Crm\Contacts\ApiException as ContactApiException;
use HubSpot\Client\Crm\Companies\ApiException as CompanyApiException;
use HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations as ContactsWithAssociations;
use HubSpot\Client\Crm\Companies\Model\SimplePublicObjectWithAssociations as CompaniesWithAssociations;
use HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations as DealWithAssociations;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectInput;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectWithAssociations as ObjectWithAssociations;
use HubSpot\Client\Crm\Pipelines\Model\Error;
use HubSpot\Client\Crm\Pipelines\Model\PipelineStage;
use HubSpot\Client\Crm\Properties\Model\Property;
use HubSpot\Discovery\Discovery;
use Jiminny\Component\Utility\Service\ProviderRateLimiter;
use Jiminny\Exceptions\CrmException;
use Jiminny\Exceptions\RateLimitException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Jobs\Crm\NoteObject;
use Jiminny\Models\Crm\Field;
use Jiminny\Services\Crm\BaseClient;
use Jiminny\Services\Crm\Hubspot\DTO\Response\Owner;
use Jiminny\Services\SocialAccountService;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use SevenShores\Hubspot\Factory;
use SevenShores\Hubspot\Http\Response;
use Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService;
use Throwable;
/**
* @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
* @param callable(): T $apiCall
* @return T
*
* @throws RateLimitException
*/
private function executeRequest(callable $apiCall)
{
if (! $this->rateLimiter->canMakeRequest($this->config)) {
$retryAfter = $this->rateLimiter->requestAvailableIn($this->config);
$this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
]);
throw new RateLimitException(
'Hubspot rate limit reached for configuration ' . $this->config->getId(),
$retryAfter,
);
}
$this->rateLimiter->incrementRequestCount($this->config);
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function isHubspotRateLimit(Throwable $e): bool
{
return method_exists($e, 'getCode') && (int) $e->getCode() === 429;
}
private function parseRetryAfter(Throwable $e): int
{
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
return 10;
}
public function getMinimumApiVersion(): string
{
return self::MIN_API_VERSION;
}
public function getInstance(): Factory
{
return new Factory([
'key' => $this->accessToken,
'oauth2' => true,
'base_url' => $this->baseUrl,
]);
}
public function getNewInstance(): Discovery
{
return \HubSpot\Factory::createWithAccessToken($this->accessToken);
}
/**
* Secondly and daily limits for Hubspot API
*
* Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)
* Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds
* Daily: 250,000 | 500,000 | 1,000,000
*
* Official documentation states: The search endpoints are rate limited to five requests per second.
* Since with 5 RPS were still hitting secondly rate limits we lowered it to 4
*/
public function getPaginatedData(array $payload, string $type, int $offset = 0): array
{
$total = 0;
$lastId = null;
$rows = [];
foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {
$rows[] = $row;
}
return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
return $this->paginationService->getPaginatedDataGenerator(
$this,
$payload,
$type,
$offset,
$total,
$lastRecordId
);
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
$deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
));
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $batchConfig['api']->read($batchReadRequest);
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
$response = $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
$response = $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
$max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // "110"
$remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // "109"
$interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // "10000"
$body = json_decode((string) $response->getBody(), true);
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));
return $response;
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function createMeeting(array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings';
return $this->makeRequest($endpoint, 'POST', $payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function updateMeeting(string $meetingId, array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings/' . $meetingId;
return $this->makeRequest($endpoint, 'PATCH', $payload);
}
/**
* @throws \Exception
*/
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string {
try {
$noteInput = new SimplePublicObjectInput([
'properties' => [
'hs_note_body' => $body,
'hubspot_owner_id' => $ownerId,
'hs_timestamp' => $timestamp,
],
]);
// Create note
$note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);
$this->getNewInstance()->crm()->objects()->associationsApi()->create(
'note',
$note->getId(),
$this->getNoteObject($noteObject),
$objectId,
$this->getNoteAssociationType($noteObject),
);
return $note->getId();
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to create note', [
'objectId' => $objectId,
'noteObject' => $noteObject->getObjectType(),
'reason' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return null;
}
public function updateEngagement(string $objectId, array $engagement, array $metadata): void
{
$this->getInstance()->engagements()->update($objectId, $engagement, $metadata);
}
public function getEngagementData(string $engagementId): array
{
$engagement = $this->getInstance()->engagements()->get($engagementId);
return $engagement->toArray();
}
public function createEngagement(array $engagement, array $associations, array $metadata): Response
{
return $this->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
}
public function isUnauthorizedException(\Exception $e): bool
{
// Check for specific HubSpot API exception types first
if ($e instanceof BadRequest) {
// BadRequest can contain 401 status codes
return $e->getCode() === 401;
}
// Check for HTTP client exceptions with status codes
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$response = $e->getResponse();
if ($response !== null) {
return $response->getStatusCode() === 401;
}
}
// Check for Guzzle HTTP exceptions
if ($e instanceof \GuzzleHttp\Exception\ClientException) {
return $e->getCode() === 401;
}
// Fallback to string matching as last resort, but be more specific
$message = strtolower($e->getMessage());
return str_contains($message, '401 unauthorized') ||
str_contains($message, 'http 401') ||
str_contains($message, 'status code 401') ||
(preg_match('/\b401\b/', $message) && str_contains($message, 'unauthorized'));
}
/**
* Validates and refreshes the access token if needed before API requests.
* This ensures long-running processes don't fail due to token expiration.
*
* @throws SocialAccountTokenInvalidException
*/
public function ensureValidToken(): void
{
if ($this->oauthAccount === null) {
return;
}
$newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);
if ($newToken !== null) {
$this->accessToken = $newToken;
}
}
public function getConfig()
{
return $this->config;
}
// returns only active (archived=false)
public function getOwners(): array
{
return $this->getNewInstance()->crm()->owners()->getAll();
}
/**
* @param bool $archived
*
* @return array<Owner>|[]
*/
public function getOwnersArchived(bool $archived = true): array
{
$endpoint = '/crm/v3/owners';
$queryParams = [
'archived' => $archived ? 'true' : 'false',
];
$queryString = http_build_query($queryParams);
$owners = [];
try {
$response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);
$responseData = $response?->toArray();
foreach ($responseData['results'] as $result) {
try {
$owners[] = Owner::create($result);
} catch (Throwable $e) {
$this->log->error('[HubSpot] Failed to process owner data', [
'result' => $result,
'error' => $e->getMessage(),
]);
continue;
}
}
} catch (Throwable $e) {
$this->log->error('HubSpot] Failed to fetch owners', [
'archived' => $archived,
'error' => $e->getMessage(),
]);
return [];
}
return $owners;
}
public function getMeeting(string $engagementId): ObjectWithAssociations
{
return $this->getNewInstance()->crm()->objects()->basicApi()
->getById('meeting', $engagementId, null, 'contact,company,deal');
}
public function deleteEngagement(string $engagementId): void
{
$this->getInstance()->engagements()->delete((int) $engagementId);
}
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array
{
$associationData = [];
$idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);
foreach ($idChunks as $idChunk) {
try {
$batchInput = new \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId();
$batchInput->setInputs(array_map(function ($id) {
$publicObjectId = new \HubSpot\Client\Crm\Associations\Model\PublicObjectId();
$publicObjectId->setId($id);
return $publicObjectId;
}, $idChunk));
$associatedObjectsData = $this
->getNewInstance()
->crm()
->associations()
->batchApi()
->read($fromObject, $toObject, $batchInput);
if ($associatedObjectsData instanceof \HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti) {
foreach ($associatedObjectsData->getResults() as $association) {
$from = $association->getFrom()->getId();
$toAssociations = $association->getTo();
if (! empty($toAssociations)) {
$associationData[$from] = array_map(function ($item) {
return $item->getId();
}, $toAssociations);
}
}
}
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to fetch associations', [
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => $e->getMessage(),
]);
}
}
return $associationData;
}
/**
* @throws \Exception
*/
private function getNoteAssociationType(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'note_to_deal',
NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it
NoteObject::Account => 'note_to_company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
/**
* @throws \Exception
*/
private function getNoteObject(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'deal',
NoteObject::Lead, NoteObject::Contact => 'contact',
NoteObject::Account => 'company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
public function addAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/create";
return $this->makeRequest($endpoint, 'POST', $payload);
}
public function removeAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/archive";
return $this->makeRequest($endpoint, 'POST', $payload);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
2868
|
NULL
|
NULL
|
NULL
|
|
2874
|
116
|
2
|
2026-05-07T11:44:58.669252+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778154298669_m2.jpg...
|
PhpStorm
|
faVsco.js – Hubspot/Client.php
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
Editor for custom.log
Sync Changes
Hide This Notification
Code changed:
Hide
2
60
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot;
use HubSpot\Client\Crm\Deals\ApiException as DealApiException;
use HubSpot\Client\Crm\Contacts\ApiException as ContactApiException;
use HubSpot\Client\Crm\Companies\ApiException as CompanyApiException;
use HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations as ContactsWithAssociations;
use HubSpot\Client\Crm\Companies\Model\SimplePublicObjectWithAssociations as CompaniesWithAssociations;
use HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations as DealWithAssociations;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectInput;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectWithAssociations as ObjectWithAssociations;
use HubSpot\Client\Crm\Pipelines\Model\Error;
use HubSpot\Client\Crm\Pipelines\Model\PipelineStage;
use HubSpot\Client\Crm\Properties\Model\Property;
use HubSpot\Discovery\Discovery;
use Jiminny\Component\Utility\Service\ProviderRateLimiter;
use Jiminny\Exceptions\CrmException;
use Jiminny\Exceptions\RateLimitException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Jobs\Crm\NoteObject;
use Jiminny\Models\Crm\Field;
use Jiminny\Services\Crm\BaseClient;
use Jiminny\Services\Crm\Hubspot\DTO\Response\Owner;
use Jiminny\Services\SocialAccountService;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use SevenShores\Hubspot\Factory;
use SevenShores\Hubspot\Http\Response;
use Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService;
use Throwable;
/**
* @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
* @param callable(): T $apiCall
* @return T
*
* @throws RateLimitException
*/
private function executeRequest(callable $apiCall)
{
if (! $this->rateLimiter->canMakeRequest($this->config)) {
$retryAfter = $this->rateLimiter->requestAvailableIn($this->config);
$this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
]);
throw new RateLimitException(
'Hubspot rate limit reached for configuration ' . $this->config->getId(),
$retryAfter,
);
}
$this->rateLimiter->incrementRequestCount($this->config);
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function isHubspotRateLimit(Throwable $e): bool
{
return method_exists($e, 'getCode') && (int) $e->getCode() === 429;
}
private function parseRetryAfter(Throwable $e): int
{
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
return 10;
}
public function getMinimumApiVersion(): string
{
return self::MIN_API_VERSION;
}
public function getInstance(): Factory
{
return new Factory([
'key' => $this->accessToken,
'oauth2' => true,
'base_url' => $this->baseUrl,
]);
}
public function getNewInstance(): Discovery
{
return \HubSpot\Factory::createWithAccessToken($this->accessToken);
}
/**
* Secondly and daily limits for Hubspot API
*
* Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)
* Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds
* Daily: 250,000 | 500,000 | 1,000,000
*
* Official documentation states: The search endpoints are rate limited to five requests per second.
* Since with 5 RPS were still hitting secondly rate limits we lowered it to 4
*/
public function getPaginatedData(array $payload, string $type, int $offset = 0): array
{
$total = 0;
$lastId = null;
$rows = [];
foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {
$rows[] = $row;
}
return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
return $this->paginationService->getPaginatedDataGenerator(
$this,
$payload,
$type,
$offset,
$total,
$lastRecordId
);
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
$deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
));
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $batchConfig['api']->read($batchReadRequest);
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
$response = $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
$response = $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
$max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // "110"
$remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // "109"
$interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // "10000"
$body = json_decode((string) $response->getBody(), true);
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));
return $response;
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function createMeeting(array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings';
return $this->makeRequest($endpoint, 'POST', $payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function updateMeeting(string $meetingId, array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings/' . $meetingId;
return $this->makeRequest($endpoint, 'PATCH', $payload);
}
/**
* @throws \Exception
*/
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string {
try {
$noteInput = new SimplePublicObjectInput([
'properties' => [
'hs_note_body' => $body,
'hubspot_owner_id' => $ownerId,
'hs_timestamp' => $timestamp,
],
]);
// Create note
$note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);
$this->getNewInstance()->crm()->objects()->associationsApi()->create(
'note',
$note->getId(),
$this->getNoteObject($noteObject),
$objectId,
$this->getNoteAssociationType($noteObject),
);
return $note->getId();
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to create note', [
'objectId' => $objectId,
'noteObject' => $noteObject->getObjectType(),
'reason' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return null;
}
public function updateEngagement(string $objectId, array $engagement, array $metadata): void
{
$this->getInstance()->engagements()->update($objectId, $engagement, $metadata);
}
public function getEngagementData(string $engagementId): array
{
$engagement = $this->getInstance()->engagements()->get($engagementId);
return $engagement->toArray();
}
public function createEngagement(array $engagement, array $associations, array $metadata): Response
{
return $this->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
}
public function isUnauthorizedException(\Exception $e): bool
{
// Check for specific HubSpot API exception types first
if ($e instanceof BadRequest) {
// BadRequest can contain 401 status codes
return $e->getCode() === 401;
}
// Check for HTTP client exceptions with status codes
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$response = $e->getResponse();
if ($response !== null) {
return $response->getStatusCode() === 401;
}
}
// Check for Guzzle HTTP exceptions
if ($e instanceof \GuzzleHttp\Exception\ClientException) {
return $e->getCode() === 401;
}
// Fallback to string matching as last resort, but be more specific
$message = strtolower($e->getMessage());
return str_contains($message, '401 unauthorized') ||
str_contains($message, 'http 401') ||
str_contains($message, 'status code 401') ||
(preg_match('/\b401\b/', $message) && str_contains($message, 'unauthorized'));
}
/**
* Validates and refreshes the access token if needed before API requests.
* This ensures long-running processes don't fail due to token expiration.
*
* @throws SocialAccountTokenInvalidException
*/
public function ensureValidToken(): void
{
if ($this->oauthAccount === null) {
return;
}
$newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);
if ($newToken !== null) {
$this->accessToken = $newToken;
}
}
public function getConfig()
{
return $this->config;
}
// returns only active (archived=false)
public function getOwners(): array
{
return $this->getNewInstance()->crm()->owners()->getAll();
}
/**
* @param bool $archived
*
* @return array<Owner>|[]
*/
public function getOwnersArchived(bool $archived = true): array
{
$endpoint = '/crm/v3/owners';
$queryParams = [
'archived' => $archived ? 'true' : 'false',
];
$queryString = http_build_query($queryParams);
$owners = [];
try {
$response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);
$responseData = $response?->toArray();
foreach ($responseData['results'] as $result) {
try {
$owners[] = Owner::create($result);
} catch (Throwable $e) {
$this->log->error('[HubSpot] Failed to process owner data', [
'result' => $result,
'error' => $e->getMessage(),
]);
continue;
}
}
} catch (Throwable $e) {
$this->log->error('HubSpot] Failed to fetch owners', [
'archived' => $archived,
'error' => $e->getMessage(),
]);
return [];
}
return $owners;
}
public function getMeeting(string $engagementId): ObjectWithAssociations
{
return $this->getNewInstance()->crm()->objects()->basicApi()
->getById('meeting', $engagementId, null, 'contact,company,deal');
}
public function deleteEngagement(string $engagementId): void
{
$this->getInstance()->engagements()->delete((int) $engagementId);
}
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array
{
$associationData = [];
$idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);
foreach ($idChunks as $idChunk) {
try {
$batchInput = new \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId();
$batchInput->setInputs(array_map(function ($id) {
$publicObjectId = new \HubSpot\Client\Crm\Associations\Model\PublicObjectId();
$publicObjectId->setId($id);
return $publicObjectId;
}, $idChunk));
$associatedObjectsData = $this
->getNewInstance()
->crm()
->associations()
->batchApi()
->read($fromObject, $toObject, $batchInput);
if ($associatedObjectsData instanceof \HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti) {
foreach ($associatedObjectsData->getResults() as $association) {
$from = $association->getFrom()->getId();
$toAssociations = $association->getTo();
if (! empty($toAssociations)) {
$associationData[$from] = array_map(function ($item) {
return $item->getId();
}, $toAssociations);
}
}
}
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to fetch associations', [
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => $e->getMessage(),
]);
}
}
return $associationData;
}
/**
* @throws \Exception
*/
private function getNoteAssociationType(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'note_to_deal',
NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it
NoteObject::Account => 'note_to_company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
/**
* @throws \Exception
*/
private function getNoteObject(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'deal',
NoteObject::Lead, NoteObject::Contact => 'contact',
NoteObject::Account => 'company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
public function addAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/create";
return $this->makeRequest($endpoint, 'POST', $payload);
}
public function removeAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/archive";
return $this->makeRequest($endpoint, 'POST', $payload);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.034242023,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: master","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"Editor for custom.log","depth":4,"bounds":{"left":0.5475399,"top":0.0726257,"width":0.44082448,"height":0.9066241},"on_screen":true,"role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.007978723,"height":0.0},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"60","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.010305851,"height":0.0},"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.00731383,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.006981383,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Component\\Utility\\Service\\ProviderRateLimiter;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n private ProviderRateLimiter $rateLimiter;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager,\n ProviderRateLimiter $rateLimiter,\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n $this->rateLimiter = $rateLimiter;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Single entry point for every HubSpot API call. Enforces the per-portal\n * rate limit configured in the rate_limits table (morphed to the current\n * Configuration) and reacts to a real 429 from HubSpot by translating it\n * into a RateLimitException carrying Retry-After.\n *\n * Wrap any outbound HubSpot call (SDK or raw HTTP) like:\n *\n * $this->executeRequest(fn () => $this->getNewInstance()->crm()->...);\n *\n * @template T\n * @param callable(): T $apiCall\n * @return T\n *\n * @throws RateLimitException\n */\n private function executeRequest(callable $apiCall)\n {\n if (! $this->rateLimiter->canMakeRequest($this->config)) {\n $retryAfter = $this->rateLimiter->requestAvailableIn($this->config);\n\n $this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n ]);\n\n throw new RateLimitException(\n 'Hubspot rate limit reached for configuration ' . $this->config->getId(),\n $retryAfter,\n );\n }\n\n $this->rateLimiter->incrementRequestCount($this->config);\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n private function isHubspotRateLimit(Throwable $e): bool\n {\n return method_exists($e, 'getCode') && (int) $e->getCode() === 429;\n }\n\n private function parseRetryAfter(Throwable $e): int\n {\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n return 10;\n }\n\n public function getMinimumApiVersion(): string\n {\n return self::MIN_API_VERSION;\n }\n\n public function getInstance(): Factory\n {\n return new Factory([\n 'key' => $this->accessToken,\n 'oauth2' => true,\n 'base_url' => $this->baseUrl,\n ]);\n }\n\n public function getNewInstance(): Discovery\n {\n return \\HubSpot\\Factory::createWithAccessToken($this->accessToken);\n }\n\n /**\n * Secondly and daily limits for Hubspot API\n *\n * Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)\n * Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds\n * Daily: 250,000 | 500,000 | 1,000,000\n *\n * Official documentation states: The search endpoints are rate limited to five requests per second.\n * Since with 5 RPS were still hitting secondly rate limits we lowered it to 4\n */\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array\n {\n $total = 0;\n $lastId = null;\n $rows = [];\n foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {\n $rows[] = $row;\n }\n\n return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n return $this->paginationService->getPaginatedDataGenerator(\n $this,\n $payload,\n $type,\n $offset,\n $total,\n $lastRecordId\n );\n }\n\n /**\n * @throws DealApiException\n * @throws CrmException\n */\n public function getOpportunityById(string $crmId, array $fields): array\n {\n try {\n $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n ));\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n $response = $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n $response = $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n\n $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // \"110\"\n $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // \"109\"\n $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // \"10000\"\n $body = json_decode((string) $response->getBody(), true);\n\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));\n\n return $response;\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Component\\Utility\\Service\\ProviderRateLimiter;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n private ProviderRateLimiter $rateLimiter;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager,\n ProviderRateLimiter $rateLimiter,\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n $this->rateLimiter = $rateLimiter;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Single entry point for every HubSpot API call. Enforces the per-portal\n * rate limit configured in the rate_limits table (morphed to the current\n * Configuration) and reacts to a real 429 from HubSpot by translating it\n * into a RateLimitException carrying Retry-After.\n *\n * Wrap any outbound HubSpot call (SDK or raw HTTP) like:\n *\n * $this->executeRequest(fn () => $this->getNewInstance()->crm()->...);\n *\n * @template T\n * @param callable(): T $apiCall\n * @return T\n *\n * @throws RateLimitException\n */\n private function executeRequest(callable $apiCall)\n {\n if (! $this->rateLimiter->canMakeRequest($this->config)) {\n $retryAfter = $this->rateLimiter->requestAvailableIn($this->config);\n\n $this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n ]);\n\n throw new RateLimitException(\n 'Hubspot rate limit reached for configuration ' . $this->config->getId(),\n $retryAfter,\n );\n }\n\n $this->rateLimiter->incrementRequestCount($this->config);\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n private function isHubspotRateLimit(Throwable $e): bool\n {\n return method_exists($e, 'getCode') && (int) $e->getCode() === 429;\n }\n\n private function parseRetryAfter(Throwable $e): int\n {\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n return 10;\n }\n\n public function getMinimumApiVersion(): string\n {\n return self::MIN_API_VERSION;\n }\n\n public function getInstance(): Factory\n {\n return new Factory([\n 'key' => $this->accessToken,\n 'oauth2' => true,\n 'base_url' => $this->baseUrl,\n ]);\n }\n\n public function getNewInstance(): Discovery\n {\n return \\HubSpot\\Factory::createWithAccessToken($this->accessToken);\n }\n\n /**\n * Secondly and daily limits for Hubspot API\n *\n * Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)\n * Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds\n * Daily: 250,000 | 500,000 | 1,000,000\n *\n * Official documentation states: The search endpoints are rate limited to five requests per second.\n * Since with 5 RPS were still hitting secondly rate limits we lowered it to 4\n */\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array\n {\n $total = 0;\n $lastId = null;\n $rows = [];\n foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {\n $rows[] = $row;\n }\n\n return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n return $this->paginationService->getPaginatedDataGenerator(\n $this,\n $payload,\n $type,\n $offset,\n $total,\n $lastRecordId\n );\n }\n\n /**\n * @throws DealApiException\n * @throws CrmException\n */\n public function getOpportunityById(string $crmId, array $fields): array\n {\n try {\n $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n ));\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n $response = $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n $response = $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n\n $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // \"110\"\n $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // \"109\"\n $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // \"10000\"\n $body = json_decode((string) $response->getBody(), true);\n\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));\n\n return $response;\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-188086479456983350
|
6378757184196315236
|
visual_change
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
Editor for custom.log
Sync Changes
Hide This Notification
Code changed:
Hide
2
60
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot;
use HubSpot\Client\Crm\Deals\ApiException as DealApiException;
use HubSpot\Client\Crm\Contacts\ApiException as ContactApiException;
use HubSpot\Client\Crm\Companies\ApiException as CompanyApiException;
use HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations as ContactsWithAssociations;
use HubSpot\Client\Crm\Companies\Model\SimplePublicObjectWithAssociations as CompaniesWithAssociations;
use HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations as DealWithAssociations;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectInput;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectWithAssociations as ObjectWithAssociations;
use HubSpot\Client\Crm\Pipelines\Model\Error;
use HubSpot\Client\Crm\Pipelines\Model\PipelineStage;
use HubSpot\Client\Crm\Properties\Model\Property;
use HubSpot\Discovery\Discovery;
use Jiminny\Component\Utility\Service\ProviderRateLimiter;
use Jiminny\Exceptions\CrmException;
use Jiminny\Exceptions\RateLimitException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Jobs\Crm\NoteObject;
use Jiminny\Models\Crm\Field;
use Jiminny\Services\Crm\BaseClient;
use Jiminny\Services\Crm\Hubspot\DTO\Response\Owner;
use Jiminny\Services\SocialAccountService;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use SevenShores\Hubspot\Factory;
use SevenShores\Hubspot\Http\Response;
use Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService;
use Throwable;
/**
* @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
* @param callable(): T $apiCall
* @return T
*
* @throws RateLimitException
*/
private function executeRequest(callable $apiCall)
{
if (! $this->rateLimiter->canMakeRequest($this->config)) {
$retryAfter = $this->rateLimiter->requestAvailableIn($this->config);
$this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
]);
throw new RateLimitException(
'Hubspot rate limit reached for configuration ' . $this->config->getId(),
$retryAfter,
);
}
$this->rateLimiter->incrementRequestCount($this->config);
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function isHubspotRateLimit(Throwable $e): bool
{
return method_exists($e, 'getCode') && (int) $e->getCode() === 429;
}
private function parseRetryAfter(Throwable $e): int
{
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
return 10;
}
public function getMinimumApiVersion(): string
{
return self::MIN_API_VERSION;
}
public function getInstance(): Factory
{
return new Factory([
'key' => $this->accessToken,
'oauth2' => true,
'base_url' => $this->baseUrl,
]);
}
public function getNewInstance(): Discovery
{
return \HubSpot\Factory::createWithAccessToken($this->accessToken);
}
/**
* Secondly and daily limits for Hubspot API
*
* Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)
* Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds
* Daily: 250,000 | 500,000 | 1,000,000
*
* Official documentation states: The search endpoints are rate limited to five requests per second.
* Since with 5 RPS were still hitting secondly rate limits we lowered it to 4
*/
public function getPaginatedData(array $payload, string $type, int $offset = 0): array
{
$total = 0;
$lastId = null;
$rows = [];
foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {
$rows[] = $row;
}
return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
return $this->paginationService->getPaginatedDataGenerator(
$this,
$payload,
$type,
$offset,
$total,
$lastRecordId
);
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
$deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
));
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $batchConfig['api']->read($batchReadRequest);
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
$response = $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
$response = $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
$max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // "110"
$remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // "109"
$interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // "10000"
$body = json_decode((string) $response->getBody(), true);
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));
return $response;
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function createMeeting(array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings';
return $this->makeRequest($endpoint, 'POST', $payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function updateMeeting(string $meetingId, array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings/' . $meetingId;
return $this->makeRequest($endpoint, 'PATCH', $payload);
}
/**
* @throws \Exception
*/
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string {
try {
$noteInput = new SimplePublicObjectInput([
'properties' => [
'hs_note_body' => $body,
'hubspot_owner_id' => $ownerId,
'hs_timestamp' => $timestamp,
],
]);
// Create note
$note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);
$this->getNewInstance()->crm()->objects()->associationsApi()->create(
'note',
$note->getId(),
$this->getNoteObject($noteObject),
$objectId,
$this->getNoteAssociationType($noteObject),
);
return $note->getId();
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to create note', [
'objectId' => $objectId,
'noteObject' => $noteObject->getObjectType(),
'reason' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return null;
}
public function updateEngagement(string $objectId, array $engagement, array $metadata): void
{
$this->getInstance()->engagements()->update($objectId, $engagement, $metadata);
}
public function getEngagementData(string $engagementId): array
{
$engagement = $this->getInstance()->engagements()->get($engagementId);
return $engagement->toArray();
}
public function createEngagement(array $engagement, array $associations, array $metadata): Response
{
return $this->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
}
public function isUnauthorizedException(\Exception $e): bool
{
// Check for specific HubSpot API exception types first
if ($e instanceof BadRequest) {
// BadRequest can contain 401 status codes
return $e->getCode() === 401;
}
// Check for HTTP client exceptions with status codes
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$response = $e->getResponse();
if ($response !== null) {
return $response->getStatusCode() === 401;
}
}
// Check for Guzzle HTTP exceptions
if ($e instanceof \GuzzleHttp\Exception\ClientException) {
return $e->getCode() === 401;
}
// Fallback to string matching as last resort, but be more specific
$message = strtolower($e->getMessage());
return str_contains($message, '401 unauthorized') ||
str_contains($message, 'http 401') ||
str_contains($message, 'status code 401') ||
(preg_match('/\b401\b/', $message) && str_contains($message, 'unauthorized'));
}
/**
* Validates and refreshes the access token if needed before API requests.
* This ensures long-running processes don't fail due to token expiration.
*
* @throws SocialAccountTokenInvalidException
*/
public function ensureValidToken(): void
{
if ($this->oauthAccount === null) {
return;
}
$newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);
if ($newToken !== null) {
$this->accessToken = $newToken;
}
}
public function getConfig()
{
return $this->config;
}
// returns only active (archived=false)
public function getOwners(): array
{
return $this->getNewInstance()->crm()->owners()->getAll();
}
/**
* @param bool $archived
*
* @return array<Owner>|[]
*/
public function getOwnersArchived(bool $archived = true): array
{
$endpoint = '/crm/v3/owners';
$queryParams = [
'archived' => $archived ? 'true' : 'false',
];
$queryString = http_build_query($queryParams);
$owners = [];
try {
$response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);
$responseData = $response?->toArray();
foreach ($responseData['results'] as $result) {
try {
$owners[] = Owner::create($result);
} catch (Throwable $e) {
$this->log->error('[HubSpot] Failed to process owner data', [
'result' => $result,
'error' => $e->getMessage(),
]);
continue;
}
}
} catch (Throwable $e) {
$this->log->error('HubSpot] Failed to fetch owners', [
'archived' => $archived,
'error' => $e->getMessage(),
]);
return [];
}
return $owners;
}
public function getMeeting(string $engagementId): ObjectWithAssociations
{
return $this->getNewInstance()->crm()->objects()->basicApi()
->getById('meeting', $engagementId, null, 'contact,company,deal');
}
public function deleteEngagement(string $engagementId): void
{
$this->getInstance()->engagements()->delete((int) $engagementId);
}
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array
{
$associationData = [];
$idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);
foreach ($idChunks as $idChunk) {
try {
$batchInput = new \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId();
$batchInput->setInputs(array_map(function ($id) {
$publicObjectId = new \HubSpot\Client\Crm\Associations\Model\PublicObjectId();
$publicObjectId->setId($id);
return $publicObjectId;
}, $idChunk));
$associatedObjectsData = $this
->getNewInstance()
->crm()
->associations()
->batchApi()
->read($fromObject, $toObject, $batchInput);
if ($associatedObjectsData instanceof \HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti) {
foreach ($associatedObjectsData->getResults() as $association) {
$from = $association->getFrom()->getId();
$toAssociations = $association->getTo();
if (! empty($toAssociations)) {
$associationData[$from] = array_map(function ($item) {
return $item->getId();
}, $toAssociations);
}
}
}
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to fetch associations', [
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => $e->getMessage(),
]);
}
}
return $associationData;
}
/**
* @throws \Exception
*/
private function getNoteAssociationType(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'note_to_deal',
NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it
NoteObject::Account => 'note_to_company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
/**
* @throws \Exception
*/
private function getNoteObject(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'deal',
NoteObject::Lead, NoteObject::Contact => 'contact',
NoteObject::Account => 'company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
public function addAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/create";
return $this->makeRequest($endpoint, 'POST', $payload);
}
public function removeAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/archive";
return $this->makeRequest($endpoint, 'POST', $payload);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
2873
|
116
|
1
|
2026-05-07T11:44:55.653768+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778154295653_m2.jpg...
|
PhpStorm
|
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Inherited members (⌘R)
Anonymous Classes (⌘I)
Lamb Inherited members (⌘R)
Anonymous Classes (⌘I)
Lambdas (⌘L)
Client, class
ASSOCIATIONS_BATCH_SIZE_LIMIT: int = 1000, public
BASE_URL: string = 'https://api.hubapi...., public
MIN_API_VERSION: string = '2' ↑BaseClient, public
paginationService: HubspotPaginationService, private
rateLimiter: ProviderRateLimiter, private
tokenManager: HubspotTokenManager, private
__construct(socialAccountService: SocialAccountService, paginationService: HubspotPaginationService, tokenManager: HubspotTokenManager, rateLimiter: ProviderRateLimiter) ↑BaseClient, public method
addAssociations(objectType: string, associationType: string, payload: array): Response, public method
batchReadObjects(objectType: string, crmIds: string[], fields: string[]): array[], private method
createBatchConfiguration(objectType: string): array, private method
createEngagement(engagement: array, associations: array, metadata: array): Response, public method
createMeeting(payload: array): Response ↑HubspotClientInterface, public method
createNote(body: string, ownerId: string, timestamp: int, objectId: string, noteObject: NoteObject): null|string ↑HubspotClientInterface, public method
deleteEngagement(engagementId: string): void, public method
ensureValidToken(): void, public method
executeRequest(apiCall: callable), private method
extractMeetingTypeOptions(endpoint: string): array, private method
fetchCallActivityTypes(): array, public method
fetchCallDispositions(): array[], public method
fetchDispositionFieldOptions(): array[], public method
fetchMeetingOutcomeFieldOptions(field: Field): array[], public method
fetchMeetingOutcomeTypes(): array, public method
fetchOpportunityFieldOptions(field: Field): array[], public method
fetchOpportunityPipelines(): array, public method
fetchOpportunityPipelineStages(): array[], public method
fetchProperty(objectType: string, propertyId: string): Property, public method
fetchPropertyOptions(objectType: string, propertyId: string): array[], public method
getAccountById(crmId: string, fields: array): array ↑HubspotClientInterface, public method
getAssociationsData(ids: array, fromObject: string, toObject: string): array ↑HubspotClientInterface, public method
getCompaniesByIds(crmIds: string[], fields: string[]): array[] ↑HubspotClientInterface, public method
getConfig(): Configuration, public method
getContactByEmail(email: string, [fields: array = [...]]): array, public method
getContactById(crmId: string, fields: array): array ↑HubspotClientInterface, public method
getContactsByIds(crmIds: string[], fields: string[]): array[] ↑HubspotClientInterface, public method
getEngagementData(engagementId: string): array ↑HubspotClientInterface, public method
getInstance(): Factory ↑HubspotClientInterface, public method
getMeeting(engagementId: string): SimplePublicObjectWithAssociations, public method
getMinimumApiVersion(): string ↑ClientInterface, public method
getNewInstance(): Discovery ↑HubspotClientInterface, public method
getNoteAssociationType(noteObject: NoteObject): string, private method
getNoteObject(noteObject: NoteObject): string, private method
getOpportunitiesByIds(crmIds: string[], fields: string[]): array[] ↑HubspotClientInterface, public method
getOpportunityById(crmId: string, fields: array): array, public method
getOwners(): array ↑HubspotClientInterface, public method
getOwnersArchived([archived: bool = true]): Owner[], public method
getPaginatedData(payload: array, type: string, [offset: int = 0]): array ↑HubspotClientInterface, public method
getPaginatedDataGenerator(payload: array, type: string, [offset: int = 0], [&total: int = 0], [&lastRecordId: null|string = null]): Generator ↑HubspotClientInterface, public method
handleBatchError(e: Throwable, objectType: string, crmIds: array): void, private method
isHubspotRateLimit(e: Throwable): bool, private method
isUnauthorizedException(e: Exception): bool, public method
logBatchResults(objectType: string, crmIds: array, results: array): void, private method
makeRequest(endpoint: string, [method: string = 'GET'], [payload: array = [...]], [queryString: null|string = null]): null|ResponseInterface|Response, public method
parseRetryAfter(e: Throwable): int, private method
prepareBatchRequest(batchConfig: array, crmIds: array, fields: array): mixed|object, private method
processApiResults(response): array, private method
removeAssociations(objectType: string, associationType: string, payload: array): Response, public method
updateEngagement(objectId: string, engagement: array, metadata: array): void, public method
updateMeeting(meetingId: string, payload: array): Response, public method
validateApiResponse(response, objectType: string): void, private method
validateBatchSize(objectType: string, crmIds: array): void, private method
ASSOCIATIONS_BATCH_SIZE_LIMIT: int = 1000, public
BASE_URL: string = 'https://api.hubapi...., public
MIN_API_VERSION: string = '2' ↑BaseClient, public
paginationService: HubspotPaginationService, private
rateLimiter: ProviderRateLimiter, private
tokenManager: HubspotTokenManager, private
__construct(socialAccountService: SocialAccountService, paginationService: HubspotPaginationService, tokenManager: HubspotTokenManager, rateLimiter: ProviderRateLimiter) ↑BaseClient, public method
addAssociations(objectType: string, associationType: string, payload: array): Response, public method
batchReadObjects(objectType: string, crmIds: string[], fields: string[]): array[], private method
createBatchConfiguration(objectType: string): array, private method
createEngagement(engagement: array, associations: array, metadata: array): Response, public method
createMeeting(payload: array): Response ↑HubspotClientInterface, public method
createNote(body: string, ownerId: string, timestamp: int, objectId: string, noteObject: NoteObject): null|string ↑HubspotClientInterface, public method
deleteEngagement(engagementId: string): void, public method
ensureValidToken(): void, public method
executeRequest(apiCall: callable), private method
extractMeetingTypeOptions(endpoint: string): array, private method
fetchCallActivityTypes(): array, public method
fetchCallDispositions(): array[], public method
fetchDispositionFieldOptions(): array[], public method
fetchMeetingOutcomeFieldOptions(field: Field): array[], public method
fetchMeetingOutcomeTypes(): array, public method
fetchOpportunityFieldOptions(field: Field): array[], public method
fetchOpportunityPipelines(): array, public method
fetchOpportunityPipelineStages(): array[], public method
fetchProperty(objectType: string, propertyId: string): Property, public method
fetchPropertyOptions(objectType: string, propertyId: string): array[], public method
getAccountById(crmId: string, fields: array): array ↑HubspotClientInterface, public method
getAssociationsData(ids: array, fromObject: string, toObject: string): array ↑HubspotClientInterface, public method
getCompaniesByIds(crmIds: string[], fields: string[]): array[] ↑HubspotClientInterface, public method
getConfig(): Configuration, public method
getContactByEmail(email: string, [fields: array = [...]]): array, public method
getContactById(crmId: string, fields: array): array ↑HubspotClientInterface, public method
getContactsByIds(crmIds: string[], fields: string[]): array[] ↑HubspotClientInterface, public method
getEngagementData(engagementId: string): array ↑HubspotClientInterface, public method
getInstance(): Factory ↑HubspotClientInterface, public method
getMeeting(engagementId: string): SimplePublicObjectWithAssociations, public method
getMinimumApiVersion(): string ↑ClientInterface, public method
getNewInstance(): Discovery ↑HubspotClientInterface, public method
getNoteAssociationType(noteObject: NoteObject): string, private method
getNoteObject(noteObject: NoteObject): string, private method
getOpportunitiesByIds(crmIds: string[], fields: string[]): array[] ↑HubspotClientInterface, public method
getOpportunityById(crmId: string, fields: array): array, public method
getOwners(): array ↑HubspotClientInterface, public method
getOwnersArchived([archived: bool = true]): Owner[], public method
getPaginatedData(payload: array, type: string, [offset: int = 0]): array ↑HubspotClientInterface, public method
getPaginatedDataGenerator(payload: array, type: string, [offset: int = 0], [&total: int = 0], [&lastRecordId: null|string = null]): Generator ↑HubspotClientInterface, public method
handleBatchError(e: Throwable, objectType: string, crmIds: array): void, private method
isHubspotRateLimit(e: Throwable): bool, private method
isUnauthorizedException(e: Exception): bool, public method
logBatchResults(objectType: string, crmIds: array, results: array): void, private method
makeRequest(endpoint: string, [method: string = 'GET'], [payload: array = [...]], [queryString: null|string = null]): null|ResponseInterface|Response, public method
parseRetryAfter(e: Throwable): int, private method
prepareBatchRequest(batchConfig: array, crmIds: array, fields: array): mixed|object, private method
processApiResults(response): array, private method
removeAssociations(objectType: string, associationType: string, payload: array): Response, public method
updateEngagement(objectId: string, engagement: array, metadata: array): void, public method
updateMeeting(meetingId: string, payload: array): Response, public method
validateApiResponse(response, objectType: string): void, private method
validateBatchSize(objectType: string, crmIds: array): void, private method
Client.php...
|
[{"role":"AXCheckBox","text [{"role":"AXCheckBox","text":"Inherited members (⌘R)","depth":1,"bounds":{"left":0.5242686,"top":0.33998403,"width":0.052526597,"height":0.022346368},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Anonymous Classes (⌘I)","depth":1,"bounds":{"left":0.58011967,"top":0.33998403,"width":0.052526597,"height":0.022346368},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Lambdas (⌘L)","depth":1,"bounds":{"left":0.6359708,"top":0.33998403,"width":0.052526597,"height":0.022346368},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Client, class","depth":4,"bounds":{"left":0.51462764,"top":0.2753392,"width":0.019946808,"height":0.017557861},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ASSOCIATIONS_BATCH_SIZE_LIMIT: int = 1000, public","depth":5,"bounds":{"left":0.5209442,"top":0.29289705,"width":0.109707445,"height":0.017557861},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"BASE_URL: string = 'https://api.hubapi...., public","depth":5,"bounds":{"left":0.5209442,"top":0.3104549,"width":0.09607713,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"MIN_API_VERSION: string = '2' ↑BaseClient, public","depth":5,"bounds":{"left":0.5209442,"top":0.32801276,"width":0.10305851,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"paginationService: HubspotPaginationService, private","depth":5,"bounds":{"left":0.5209442,"top":0.34557062,"width":0.106715426,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"rateLimiter: ProviderRateLimiter, private","depth":5,"bounds":{"left":0.5209442,"top":0.36312848,"width":0.078457445,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"tokenManager: HubspotTokenManager, private","depth":5,"bounds":{"left":0.5209442,"top":0.38068634,"width":0.09375,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"__construct(socialAccountService: SocialAccountService, paginationService: HubspotPaginationService, tokenManager: HubspotTokenManager, rateLimiter: ProviderRateLimiter) ↑BaseClient, public method","depth":5,"bounds":{"left":0.5209442,"top":0.3982442,"width":0.40425533,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"addAssociations(objectType: string, associationType: string, payload: array): Response, public method","depth":5,"bounds":{"left":0.5209442,"top":0.41580206,"width":0.19115691,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"batchReadObjects(objectType: string, crmIds: string[], fields: string[]): array[], private method","depth":5,"bounds":{"left":0.5209442,"top":0.43335995,"width":0.17154256,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"createBatchConfiguration(objectType: string): array, private method","depth":5,"bounds":{"left":0.5209442,"top":0.4509178,"width":0.11934841,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"createEngagement(engagement: array, associations: array, metadata: array): Response, public method","depth":5,"bounds":{"left":0.5209442,"top":0.46847567,"width":0.19182181,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"createMeeting(payload: array): Response ↑HubspotClientInterface, public method","depth":5,"bounds":{"left":0.5209442,"top":0.48603353,"width":0.15159574,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"createNote(body: string, ownerId: string, timestamp: int, objectId: string, noteObject: NoteObject): null|string ↑HubspotClientInterface, public method","depth":5,"bounds":{"left":0.5209442,"top":0.50359136,"width":0.28856382,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"deleteEngagement(engagementId: string): void, public method","depth":5,"bounds":{"left":0.5209442,"top":0.5211492,"width":0.109707445,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"ensureValidToken(): void, public method","depth":5,"bounds":{"left":0.5209442,"top":0.5387071,"width":0.064494684,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"executeRequest(apiCall: callable), private method","depth":5,"bounds":{"left":0.5209442,"top":0.55626494,"width":0.08144947,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"extractMeetingTypeOptions(endpoint: string): array, private method","depth":5,"bounds":{"left":0.5209442,"top":0.5738228,"width":0.119015954,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"fetchCallActivityTypes(): array, public method","depth":5,"bounds":{"left":0.5209442,"top":0.5913807,"width":0.07579787,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"fetchCallDispositions(): array[], public method","depth":5,"bounds":{"left":0.5209442,"top":0.6089386,"width":0.07579787,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"fetchDispositionFieldOptions(): array[], public method","depth":5,"bounds":{"left":0.5209442,"top":0.62649643,"width":0.09142287,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"fetchMeetingOutcomeFieldOptions(field: Field): array[], public method","depth":5,"bounds":{"left":0.5209442,"top":0.6440543,"width":0.12533244,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"fetchMeetingOutcomeTypes(): array, public method","depth":5,"bounds":{"left":0.5209442,"top":0.66161215,"width":0.08809841,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"fetchOpportunityFieldOptions(field: Field): array[], public method","depth":5,"bounds":{"left":0.5209442,"top":0.67917,"width":0.1143617,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"fetchOpportunityPipelines(): array, public method","depth":5,"bounds":{"left":0.5209442,"top":0.6967279,"width":0.08277926,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"fetchOpportunityPipelineStages(): array[], public method","depth":5,"bounds":{"left":0.5209442,"top":0.71428573,"width":0.09773936,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"fetchProperty(objectType: string, propertyId: string): Property, public method","depth":5,"bounds":{"left":0.5209442,"top":0.7318436,"width":0.13996011,"height":0.017557861},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"fetchPropertyOptions(objectType: string, propertyId: string): array[], public method","depth":5,"bounds":{"left":0.5209442,"top":0.74940145,"width":0.15192819,"height":0.017557861},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"getAccountById(crmId: string, fields: array): array ↑HubspotClientInterface, public method","depth":5,"bounds":{"left":0.5209442,"top":0.7669593,"width":0.16788563,"height":0.017557861},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"getAssociationsData(ids: array, fromObject: string, toObject: string): array ↑HubspotClientInterface, public method","depth":5,"bounds":{"left":0.5209442,"top":0.78451717,"width":0.21775267,"height":0.017557861},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"getCompaniesByIds(crmIds: string[], fields: string[]): array[] ↑HubspotClientInterface, public method","depth":5,"bounds":{"left":0.5209442,"top":0.802075,"width":0.1888298,"height":0.017557861},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"getConfig(): Configuration, public method","depth":5,"bounds":{"left":0.5209442,"top":0.8196329,"width":0.066821806,"height":0.017557861},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"getContactByEmail(email: string, [fields: array = [...]]): array, public method","depth":5,"bounds":{"left":0.5209442,"top":0.83719075,"width":0.1349734,"height":0.017557861},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"getContactById(crmId: string, fields: array): array ↑HubspotClientInterface, public method","depth":5,"bounds":{"left":0.5209442,"top":0.8547486,"width":0.16722074,"height":0.017557861},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"getContactsByIds(crmIds: string[], fields: string[]): array[] ↑HubspotClientInterface, public method","depth":5,"bounds":{"left":0.5209442,"top":0.87230647,"width":0.18450798,"height":0.017557861},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"getEngagementData(engagementId: string): array ↑HubspotClientInterface, public method","depth":5,"bounds":{"left":0.5209442,"top":0.8898643,"width":0.16855054,"height":0.017557861},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"getInstance(): Factory ↑HubspotClientInterface, public method","depth":5,"bounds":{"left":0.5209442,"top":0.9074222,"width":0.11236702,"height":0.017557861},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"getMeeting(engagementId: string): SimplePublicObjectWithAssociations, public method","depth":5,"bounds":{"left":0.5209442,"top":0.92498004,"width":0.16057181,"height":0.017557861},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"getMinimumApiVersion(): string ↑ClientInterface, public method","depth":5,"bounds":{"left":0.5209442,"top":0.9425379,"width":0.11402926,"height":0.017557861},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"getNewInstance(): Discovery ↑HubspotClientInterface, public method","depth":5,"bounds":{"left":0.5209442,"top":0.96009576,"width":0.1263298,"height":0.017557861},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"getNoteAssociationType(noteObject: NoteObject): string, private method","depth":5,"bounds":{"left":0.5209442,"top":0.9776536,"width":0.12965426,"height":0.017557861},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"getNoteObject(noteObject: NoteObject): string, private method","depth":5,"bounds":{"left":0.5209442,"top":0.9952115,"width":0.109375,"height":0.004788518},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"getOpportunitiesByIds(crmIds: string[], fields: string[]): array[] ↑HubspotClientInterface, public method","depth":5,"bounds":{"left":0.5209442,"top":1.0,"width":0.19381648,"height":-0.0127693415},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"getOpportunityById(crmId: string, fields: array): array, public method","depth":5,"bounds":{"left":0.5209442,"top":1.0,"width":0.12167553,"height":-0.0303272},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"getOwners(): array ↑HubspotClientInterface, public method","depth":5,"bounds":{"left":0.5209442,"top":1.0,"width":0.10571808,"height":-0.04788506},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"getOwnersArchived([archived: bool = true]): Owner[], public method","depth":5,"bounds":{"left":0.5209442,"top":1.0,"width":0.12167553,"height":-0.06544292},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"getPaginatedData(payload: array, type: string, [offset: int = 0]): array ↑HubspotClientInterface, public method","depth":5,"bounds":{"left":0.5209442,"top":1.0,"width":0.20777926,"height":-0.08300078},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"getPaginatedDataGenerator(payload: array, type: string, [offset: int = 0], [&total: int = 0], [&lastRecordId: null|string = null]): Generator ↑HubspotClientInterface, public method","depth":5,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"handleBatchError(e: Throwable, objectType: string, crmIds: array): void, private method","depth":5,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"isHubspotRateLimit(e: Throwable): bool, private method","depth":5,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"isUnauthorizedException(e: Exception): bool, public method","depth":5,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"logBatchResults(objectType: string, crmIds: array, results: array): void, private method","depth":5,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"makeRequest(endpoint: string, [method: string = 'GET'], [payload: array = [...]], [queryString: null|string = null]): null|ResponseInterface|Response, public method","depth":5,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"parseRetryAfter(e: Throwable): int, private method","depth":5,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"prepareBatchRequest(batchConfig: array, crmIds: array, fields: array): mixed|object, private method","depth":5,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"processApiResults(response): array, private method","depth":5,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"removeAssociations(objectType: string, associationType: string, payload: array): Response, public method","depth":5,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"updateEngagement(objectId: string, engagement: array, metadata: array): void, public method","depth":5,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"updateMeeting(meetingId: string, payload: array): Response, public method","depth":5,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"validateApiResponse(response, objectType: string): void, private method","depth":5,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"validateBatchSize(objectType: string, crmIds: array): void, private method","depth":5,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ASSOCIATIONS_BATCH_SIZE_LIMIT: int = 1000, public","depth":4,"bounds":{"left":0.5209442,"top":0.29289705,"width":0.109707445,"height":0.017557861},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"BASE_URL: string = 'https://api.hubapi...., public","depth":4,"bounds":{"left":0.5209442,"top":0.3104549,"width":0.09607713,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"MIN_API_VERSION: string = '2' ↑BaseClient, public","depth":4,"bounds":{"left":0.5209442,"top":0.32801276,"width":0.10305851,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"paginationService: HubspotPaginationService, private","depth":4,"bounds":{"left":0.5209442,"top":0.34557062,"width":0.106715426,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"rateLimiter: ProviderRateLimiter, private","depth":4,"bounds":{"left":0.5209442,"top":0.36312848,"width":0.078457445,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"tokenManager: HubspotTokenManager, private","depth":4,"bounds":{"left":0.5209442,"top":0.38068634,"width":0.09375,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"__construct(socialAccountService: SocialAccountService, paginationService: HubspotPaginationService, tokenManager: HubspotTokenManager, rateLimiter: ProviderRateLimiter) ↑BaseClient, public method","depth":4,"bounds":{"left":0.5209442,"top":0.3982442,"width":0.40425533,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"addAssociations(objectType: string, associationType: string, payload: array): Response, public method","depth":4,"bounds":{"left":0.5209442,"top":0.41580206,"width":0.19115691,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"batchReadObjects(objectType: string, crmIds: string[], fields: string[]): array[], private method","depth":4,"bounds":{"left":0.5209442,"top":0.43335995,"width":0.17154256,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"createBatchConfiguration(objectType: string): array, private method","depth":4,"bounds":{"left":0.5209442,"top":0.4509178,"width":0.11934841,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"createEngagement(engagement: array, associations: array, metadata: array): Response, public method","depth":4,"bounds":{"left":0.5209442,"top":0.46847567,"width":0.19182181,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"createMeeting(payload: array): Response ↑HubspotClientInterface, public method","depth":4,"bounds":{"left":0.5209442,"top":0.48603353,"width":0.15159574,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"createNote(body: string, ownerId: string, timestamp: int, objectId: string, noteObject: NoteObject): null|string ↑HubspotClientInterface, public method","depth":4,"bounds":{"left":0.5209442,"top":0.50359136,"width":0.28856382,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"deleteEngagement(engagementId: string): void, public method","depth":4,"bounds":{"left":0.5209442,"top":0.5211492,"width":0.109707445,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"ensureValidToken(): void, public method","depth":4,"bounds":{"left":0.5209442,"top":0.5387071,"width":0.064494684,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"executeRequest(apiCall: callable), private method","depth":4,"bounds":{"left":0.5209442,"top":0.55626494,"width":0.08144947,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"extractMeetingTypeOptions(endpoint: string): array, private method","depth":4,"bounds":{"left":0.5209442,"top":0.5738228,"width":0.119015954,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"fetchCallActivityTypes(): array, public method","depth":4,"bounds":{"left":0.5209442,"top":0.5913807,"width":0.07579787,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"fetchCallDispositions(): array[], public method","depth":4,"bounds":{"left":0.5209442,"top":0.6089386,"width":0.07579787,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"fetchDispositionFieldOptions(): array[], public method","depth":4,"bounds":{"left":0.5209442,"top":0.62649643,"width":0.09142287,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"fetchMeetingOutcomeFieldOptions(field: Field): array[], public method","depth":4,"bounds":{"left":0.5209442,"top":0.6440543,"width":0.12533244,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"fetchMeetingOutcomeTypes(): array, public method","depth":4,"bounds":{"left":0.5209442,"top":0.66161215,"width":0.08809841,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"fetchOpportunityFieldOptions(field: Field): array[], public method","depth":4,"bounds":{"left":0.5209442,"top":0.67917,"width":0.1143617,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"fetchOpportunityPipelines(): array, public method","depth":4,"bounds":{"left":0.5209442,"top":0.6967279,"width":0.08277926,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"fetchOpportunityPipelineStages(): array[], public method","depth":4,"bounds":{"left":0.5209442,"top":0.71428573,"width":0.09773936,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"fetchProperty(objectType: string, propertyId: string): Property, public method","depth":4,"bounds":{"left":0.5209442,"top":0.7318436,"width":0.13996011,"height":0.017557861},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"fetchPropertyOptions(objectType: string, propertyId: string): array[], public method","depth":4,"bounds":{"left":0.5209442,"top":0.74940145,"width":0.15192819,"height":0.017557861},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"getAccountById(crmId: string, fields: array): array ↑HubspotClientInterface, public method","depth":4,"bounds":{"left":0.5209442,"top":0.7669593,"width":0.16788563,"height":0.017557861},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"getAssociationsData(ids: array, fromObject: string, toObject: string): array ↑HubspotClientInterface, public method","depth":4,"bounds":{"left":0.5209442,"top":0.78451717,"width":0.21775267,"height":0.017557861},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"getCompaniesByIds(crmIds: string[], fields: string[]): array[] ↑HubspotClientInterface, public method","depth":4,"bounds":{"left":0.5209442,"top":0.802075,"width":0.1888298,"height":0.017557861},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"getConfig(): Configuration, public method","depth":4,"bounds":{"left":0.5209442,"top":0.8196329,"width":0.066821806,"height":0.017557861},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"getContactByEmail(email: string, [fields: array = [...]]): array, public method","depth":4,"bounds":{"left":0.5209442,"top":0.83719075,"width":0.1349734,"height":0.017557861},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"getContactById(crmId: string, fields: array): array ↑HubspotClientInterface, public method","depth":4,"bounds":{"left":0.5209442,"top":0.8547486,"width":0.16722074,"height":0.017557861},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"getContactsByIds(crmIds: string[], fields: string[]): array[] ↑HubspotClientInterface, public method","depth":4,"bounds":{"left":0.5209442,"top":0.87230647,"width":0.18450798,"height":0.017557861},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"getEngagementData(engagementId: string): array ↑HubspotClientInterface, public method","depth":4,"bounds":{"left":0.5209442,"top":0.8898643,"width":0.16855054,"height":0.017557861},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"getInstance(): Factory ↑HubspotClientInterface, public method","depth":4,"bounds":{"left":0.5209442,"top":0.9074222,"width":0.11236702,"height":0.017557861},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"getMeeting(engagementId: string): SimplePublicObjectWithAssociations, public method","depth":4,"bounds":{"left":0.5209442,"top":0.92498004,"width":0.16057181,"height":0.017557861},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"getMinimumApiVersion(): string ↑ClientInterface, public method","depth":4,"bounds":{"left":0.5209442,"top":0.9425379,"width":0.11402926,"height":0.017557861},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"getNewInstance(): Discovery ↑HubspotClientInterface, public method","depth":4,"bounds":{"left":0.5209442,"top":0.96009576,"width":0.1263298,"height":0.017557861},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"getNoteAssociationType(noteObject: NoteObject): string, private method","depth":4,"bounds":{"left":0.5209442,"top":0.9776536,"width":0.12965426,"height":0.017557861},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"getNoteObject(noteObject: NoteObject): string, private method","depth":4,"bounds":{"left":0.5209442,"top":0.9952115,"width":0.109375,"height":0.004788518},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"getOpportunitiesByIds(crmIds: string[], fields: string[]): array[] ↑HubspotClientInterface, public method","depth":4,"bounds":{"left":0.5209442,"top":1.0,"width":0.19381648,"height":-0.0127693415},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"getOpportunityById(crmId: string, fields: array): array, public method","depth":4,"bounds":{"left":0.5209442,"top":1.0,"width":0.12167553,"height":-0.0303272},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"getOwners(): array ↑HubspotClientInterface, public method","depth":4,"bounds":{"left":0.5209442,"top":1.0,"width":0.10571808,"height":-0.04788506},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"getOwnersArchived([archived: bool = true]): Owner[], public method","depth":4,"bounds":{"left":0.5209442,"top":1.0,"width":0.12167553,"height":-0.06544292},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"getPaginatedData(payload: array, type: string, [offset: int = 0]): array ↑HubspotClientInterface, public method","depth":4,"bounds":{"left":0.5209442,"top":1.0,"width":0.20777926,"height":-0.08300078},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"getPaginatedDataGenerator(payload: array, type: string, [offset: int = 0], [&total: int = 0], [&lastRecordId: null|string = null]): Generator ↑HubspotClientInterface, public method","depth":4,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"handleBatchError(e: Throwable, objectType: string, crmIds: array): void, private method","depth":4,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"isHubspotRateLimit(e: Throwable): bool, private method","depth":4,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"isUnauthorizedException(e: Exception): bool, public method","depth":4,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"logBatchResults(objectType: string, crmIds: array, results: array): void, private method","depth":4,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"makeRequest(endpoint: string, [method: string = 'GET'], [payload: array = [...]], [queryString: null|string = null]): null|ResponseInterface|Response, public method","depth":4,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"parseRetryAfter(e: Throwable): int, private method","depth":4,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"prepareBatchRequest(batchConfig: array, crmIds: array, fields: array): mixed|object, private method","depth":4,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"processApiResults(response): array, private method","depth":4,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"removeAssociations(objectType: string, associationType: string, payload: array): Response, public method","depth":4,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"updateEngagement(objectId: string, engagement: array, metadata: array): void, public method","depth":4,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"updateMeeting(meetingId: string, payload: array): Response, public method","depth":4,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"validateApiResponse(response, objectType: string): void, private method","depth":4,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"validateBatchSize(objectType: string, crmIds: array): void, private method","depth":4,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Client.php","depth":1,"bounds":{"left":0.5242686,"top":0.31364724,"width":0.19980054,"height":0.026336791},"on_screen":true,"role_description":"text"}]...
|
8179037214445392322
|
7502678635301665467
|
visual_change
|
accessibility
|
NULL
|
Inherited members (⌘R)
Anonymous Classes (⌘I)
Lamb Inherited members (⌘R)
Anonymous Classes (⌘I)
Lambdas (⌘L)
Client, class
ASSOCIATIONS_BATCH_SIZE_LIMIT: int = 1000, public
BASE_URL: string = 'https://api.hubapi...., public
MIN_API_VERSION: string = '2' ↑BaseClient, public
paginationService: HubspotPaginationService, private
rateLimiter: ProviderRateLimiter, private
tokenManager: HubspotTokenManager, private
__construct(socialAccountService: SocialAccountService, paginationService: HubspotPaginationService, tokenManager: HubspotTokenManager, rateLimiter: ProviderRateLimiter) ↑BaseClient, public method
addAssociations(objectType: string, associationType: string, payload: array): Response, public method
batchReadObjects(objectType: string, crmIds: string[], fields: string[]): array[], private method
createBatchConfiguration(objectType: string): array, private method
createEngagement(engagement: array, associations: array, metadata: array): Response, public method
createMeeting(payload: array): Response ↑HubspotClientInterface, public method
createNote(body: string, ownerId: string, timestamp: int, objectId: string, noteObject: NoteObject): null|string ↑HubspotClientInterface, public method
deleteEngagement(engagementId: string): void, public method
ensureValidToken(): void, public method
executeRequest(apiCall: callable), private method
extractMeetingTypeOptions(endpoint: string): array, private method
fetchCallActivityTypes(): array, public method
fetchCallDispositions(): array[], public method
fetchDispositionFieldOptions(): array[], public method
fetchMeetingOutcomeFieldOptions(field: Field): array[], public method
fetchMeetingOutcomeTypes(): array, public method
fetchOpportunityFieldOptions(field: Field): array[], public method
fetchOpportunityPipelines(): array, public method
fetchOpportunityPipelineStages(): array[], public method
fetchProperty(objectType: string, propertyId: string): Property, public method
fetchPropertyOptions(objectType: string, propertyId: string): array[], public method
getAccountById(crmId: string, fields: array): array ↑HubspotClientInterface, public method
getAssociationsData(ids: array, fromObject: string, toObject: string): array ↑HubspotClientInterface, public method
getCompaniesByIds(crmIds: string[], fields: string[]): array[] ↑HubspotClientInterface, public method
getConfig(): Configuration, public method
getContactByEmail(email: string, [fields: array = [...]]): array, public method
getContactById(crmId: string, fields: array): array ↑HubspotClientInterface, public method
getContactsByIds(crmIds: string[], fields: string[]): array[] ↑HubspotClientInterface, public method
getEngagementData(engagementId: string): array ↑HubspotClientInterface, public method
getInstance(): Factory ↑HubspotClientInterface, public method
getMeeting(engagementId: string): SimplePublicObjectWithAssociations, public method
getMinimumApiVersion(): string ↑ClientInterface, public method
getNewInstance(): Discovery ↑HubspotClientInterface, public method
getNoteAssociationType(noteObject: NoteObject): string, private method
getNoteObject(noteObject: NoteObject): string, private method
getOpportunitiesByIds(crmIds: string[], fields: string[]): array[] ↑HubspotClientInterface, public method
getOpportunityById(crmId: string, fields: array): array, public method
getOwners(): array ↑HubspotClientInterface, public method
getOwnersArchived([archived: bool = true]): Owner[], public method
getPaginatedData(payload: array, type: string, [offset: int = 0]): array ↑HubspotClientInterface, public method
getPaginatedDataGenerator(payload: array, type: string, [offset: int = 0], [&total: int = 0], [&lastRecordId: null|string = null]): Generator ↑HubspotClientInterface, public method
handleBatchError(e: Throwable, objectType: string, crmIds: array): void, private method
isHubspotRateLimit(e: Throwable): bool, private method
isUnauthorizedException(e: Exception): bool, public method
logBatchResults(objectType: string, crmIds: array, results: array): void, private method
makeRequest(endpoint: string, [method: string = 'GET'], [payload: array = [...]], [queryString: null|string = null]): null|ResponseInterface|Response, public method
parseRetryAfter(e: Throwable): int, private method
prepareBatchRequest(batchConfig: array, crmIds: array, fields: array): mixed|object, private method
processApiResults(response): array, private method
removeAssociations(objectType: string, associationType: string, payload: array): Response, public method
updateEngagement(objectId: string, engagement: array, metadata: array): void, public method
updateMeeting(meetingId: string, payload: array): Response, public method
validateApiResponse(response, objectType: string): void, private method
validateBatchSize(objectType: string, crmIds: array): void, private method
ASSOCIATIONS_BATCH_SIZE_LIMIT: int = 1000, public
BASE_URL: string = 'https://api.hubapi...., public
MIN_API_VERSION: string = '2' ↑BaseClient, public
paginationService: HubspotPaginationService, private
rateLimiter: ProviderRateLimiter, private
tokenManager: HubspotTokenManager, private
__construct(socialAccountService: SocialAccountService, paginationService: HubspotPaginationService, tokenManager: HubspotTokenManager, rateLimiter: ProviderRateLimiter) ↑BaseClient, public method
addAssociations(objectType: string, associationType: string, payload: array): Response, public method
batchReadObjects(objectType: string, crmIds: string[], fields: string[]): array[], private method
createBatchConfiguration(objectType: string): array, private method
createEngagement(engagement: array, associations: array, metadata: array): Response, public method
createMeeting(payload: array): Response ↑HubspotClientInterface, public method
createNote(body: string, ownerId: string, timestamp: int, objectId: string, noteObject: NoteObject): null|string ↑HubspotClientInterface, public method
deleteEngagement(engagementId: string): void, public method
ensureValidToken(): void, public method
executeRequest(apiCall: callable), private method
extractMeetingTypeOptions(endpoint: string): array, private method
fetchCallActivityTypes(): array, public method
fetchCallDispositions(): array[], public method
fetchDispositionFieldOptions(): array[], public method
fetchMeetingOutcomeFieldOptions(field: Field): array[], public method
fetchMeetingOutcomeTypes(): array, public method
fetchOpportunityFieldOptions(field: Field): array[], public method
fetchOpportunityPipelines(): array, public method
fetchOpportunityPipelineStages(): array[], public method
fetchProperty(objectType: string, propertyId: string): Property, public method
fetchPropertyOptions(objectType: string, propertyId: string): array[], public method
getAccountById(crmId: string, fields: array): array ↑HubspotClientInterface, public method
getAssociationsData(ids: array, fromObject: string, toObject: string): array ↑HubspotClientInterface, public method
getCompaniesByIds(crmIds: string[], fields: string[]): array[] ↑HubspotClientInterface, public method
getConfig(): Configuration, public method
getContactByEmail(email: string, [fields: array = [...]]): array, public method
getContactById(crmId: string, fields: array): array ↑HubspotClientInterface, public method
getContactsByIds(crmIds: string[], fields: string[]): array[] ↑HubspotClientInterface, public method
getEngagementData(engagementId: string): array ↑HubspotClientInterface, public method
getInstance(): Factory ↑HubspotClientInterface, public method
getMeeting(engagementId: string): SimplePublicObjectWithAssociations, public method
getMinimumApiVersion(): string ↑ClientInterface, public method
getNewInstance(): Discovery ↑HubspotClientInterface, public method
getNoteAssociationType(noteObject: NoteObject): string, private method
getNoteObject(noteObject: NoteObject): string, private method
getOpportunitiesByIds(crmIds: string[], fields: string[]): array[] ↑HubspotClientInterface, public method
getOpportunityById(crmId: string, fields: array): array, public method
getOwners(): array ↑HubspotClientInterface, public method
getOwnersArchived([archived: bool = true]): Owner[], public method
getPaginatedData(payload: array, type: string, [offset: int = 0]): array ↑HubspotClientInterface, public method
getPaginatedDataGenerator(payload: array, type: string, [offset: int = 0], [&total: int = 0], [&lastRecordId: null|string = null]): Generator ↑HubspotClientInterface, public method
handleBatchError(e: Throwable, objectType: string, crmIds: array): void, private method
isHubspotRateLimit(e: Throwable): bool, private method
isUnauthorizedException(e: Exception): bool, public method
logBatchResults(objectType: string, crmIds: array, results: array): void, private method
makeRequest(endpoint: string, [method: string = 'GET'], [payload: array = [...]], [queryString: null|string = null]): null|ResponseInterface|Response, public method
parseRetryAfter(e: Throwable): int, private method
prepareBatchRequest(batchConfig: array, crmIds: array, fields: array): mixed|object, private method
processApiResults(response): array, private method
removeAssociations(objectType: string, associationType: string, payload: array): Response, public method
updateEngagement(objectId: string, engagement: array, metadata: array): void, public method
updateMeeting(meetingId: string, payload: array): Response, public method
validateApiResponse(response, objectType: string): void, private method
validateBatchSize(objectType: string, crmIds: array): void, private method
Client.php...
|
2870
|
NULL
|
NULL
|
NULL
|
|
2872
|
116
|
0
|
2026-05-07T11:44:52.938377+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778154292938_m2.jpg...
|
PhpStorm
|
faVsco.js – Hubspot/Client.php
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
Editor for custom.log
Sync Changes
Hide This Notification
Code changed:
Hide
2
60
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot;
use HubSpot\Client\Crm\Deals\ApiException as DealApiException;
use HubSpot\Client\Crm\Contacts\ApiException as ContactApiException;
use HubSpot\Client\Crm\Companies\ApiException as CompanyApiException;
use HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations as ContactsWithAssociations;
use HubSpot\Client\Crm\Companies\Model\SimplePublicObjectWithAssociations as CompaniesWithAssociations;
use HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations as DealWithAssociations;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectInput;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectWithAssociations as ObjectWithAssociations;
use HubSpot\Client\Crm\Pipelines\Model\Error;
use HubSpot\Client\Crm\Pipelines\Model\PipelineStage;
use HubSpot\Client\Crm\Properties\Model\Property;
use HubSpot\Discovery\Discovery;
use Jiminny\Component\Utility\Service\ProviderRateLimiter;
use Jiminny\Exceptions\CrmException;
use Jiminny\Exceptions\RateLimitException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Jobs\Crm\NoteObject;
use Jiminny\Models\Crm\Field;
use Jiminny\Services\Crm\BaseClient;
use Jiminny\Services\Crm\Hubspot\DTO\Response\Owner;
use Jiminny\Services\SocialAccountService;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use SevenShores\Hubspot\Factory;
use SevenShores\Hubspot\Http\Response;
use Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService;
use Throwable;
/**
* @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
* @param callable(): T $apiCall
* @return T
*
* @throws RateLimitException
*/
private function executeRequest(callable $apiCall)
{
if (! $this->rateLimiter->canMakeRequest($this->config)) {
$retryAfter = $this->rateLimiter->requestAvailableIn($this->config);
$this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
]);
throw new RateLimitException(
'Hubspot rate limit reached for configuration ' . $this->config->getId(),
$retryAfter,
);
}
$this->rateLimiter->incrementRequestCount($this->config);
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function isHubspotRateLimit(Throwable $e): bool
{
return method_exists($e, 'getCode') && (int) $e->getCode() === 429;
}
private function parseRetryAfter(Throwable $e): int
{
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
return 10;
}
public function getMinimumApiVersion(): string
{
return self::MIN_API_VERSION;
}
public function getInstance(): Factory
{
return new Factory([
'key' => $this->accessToken,
'oauth2' => true,
'base_url' => $this->baseUrl,
]);
}
public function getNewInstance(): Discovery
{
return \HubSpot\Factory::createWithAccessToken($this->accessToken);
}
/**
* Secondly and daily limits for Hubspot API
*
* Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)
* Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds
* Daily: 250,000 | 500,000 | 1,000,000
*
* Official documentation states: The search endpoints are rate limited to five requests per second.
* Since with 5 RPS were still hitting secondly rate limits we lowered it to 4
*/
public function getPaginatedData(array $payload, string $type, int $offset = 0): array
{
$total = 0;
$lastId = null;
$rows = [];
foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {
$rows[] = $row;
}
return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
return $this->paginationService->getPaginatedDataGenerator(
$this,
$payload,
$type,
$offset,
$total,
$lastRecordId
);
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
$deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
));
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $batchConfig['api']->read($batchReadRequest);
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
$response = $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
$response = $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
$max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // "110"
$remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // "109"
$interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // "10000"
$body = json_decode((string) $response->getBody(), true);
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));
return $response;
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function createMeeting(array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings';
return $this->makeRequest($endpoint, 'POST', $payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function updateMeeting(string $meetingId, array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings/' . $meetingId;
return $this->makeRequest($endpoint, 'PATCH', $payload);
}
/**
* @throws \Exception
*/
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string {
try {
$noteInput = new SimplePublicObjectInput([
'properties' => [
'hs_note_body' => $body,
'hubspot_owner_id' => $ownerId,
'hs_timestamp' => $timestamp,
],
]);
// Create note
$note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);
$this->getNewInstance()->crm()->objects()->associationsApi()->create(
'note',
$note->getId(),
$this->getNoteObject($noteObject),
$objectId,
$this->getNoteAssociationType($noteObject),
);
return $note->getId();
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to create note', [
'objectId' => $objectId,
'noteObject' => $noteObject->getObjectType(),
'reason' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return null;
}
public function updateEngagement(string $objectId, array $engagement, array $metadata): void
{
$this->getInstance()->engagements()->update($objectId, $engagement, $metadata);
}
public function getEngagementData(string $engagementId): array
{
$engagement = $this->getInstance()->engagements()->get($engagementId);
return $engagement->toArray();
}
public function createEngagement(array $engagement, array $associations, array $metadata): Response
{
return $this->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
}
public function isUnauthorizedException(\Exception $e): bool
{
// Check for specific HubSpot API exception types first
if ($e instanceof BadRequest) {
// BadRequest can contain 401 status codes
return $e->getCode() === 401;
}
// Check for HTTP client exceptions with status codes
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$response = $e->getResponse();
if ($response !== null) {
return $response->getStatusCode() === 401;
}
}
// Check for Guzzle HTTP exceptions
if ($e instanceof \GuzzleHttp\Exception\ClientException) {
return $e->getCode() === 401;
}
// Fallback to string matching as last resort, but be more specific
$message = strtolower($e->getMessage());
return str_contains($message, '401 unauthorized') ||
str_contains($message, 'http 401') ||
str_contains($message, 'status code 401') ||
(preg_match('/\b401\b/', $message) && str_contains($message, 'unauthorized'));
}
/**
* Validates and refreshes the access token if needed before API requests.
* This ensures long-running processes don't fail due to token expiration.
*
* @throws SocialAccountTokenInvalidException
*/
public function ensureValidToken(): void
{
if ($this->oauthAccount === null) {
return;
}
$newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);
if ($newToken !== null) {
$this->accessToken = $newToken;
}
}
public function getConfig()
{
return $this->config;
}
// returns only active (archived=false)
public function getOwners(): array
{
return $this->getNewInstance()->crm()->owners()->getAll();
}
/**
* @param bool $archived
*
* @return array<Owner>|[]
*/
public function getOwnersArchived(bool $archived = true): array
{
$endpoint = '/crm/v3/owners';
$queryParams = [
'archived' => $archived ? 'true' : 'false',
];
$queryString = http_build_query($queryParams);
$owners = [];
try {
$response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);
$responseData = $response?->toArray();
foreach ($responseData['results'] as $result) {
try {
$owners[] = Owner::create($result);
} catch (Throwable $e) {
$this->log->error('[HubSpot] Failed to process owner data', [
'result' => $result,
'error' => $e->getMessage(),
]);
continue;
}
}
} catch (Throwable $e) {
$this->log->error('HubSpot] Failed to fetch owners', [
'archived' => $archived,
'error' => $e->getMessage(),
]);
return [];
}
return $owners;
}
public function getMeeting(string $engagementId): ObjectWithAssociations
{
return $this->getNewInstance()->crm()->objects()->basicApi()
->getById('meeting', $engagementId, null, 'contact,company,deal');
}
public function deleteEngagement(string $engagementId): void
{
$this->getInstance()->engagements()->delete((int) $engagementId);
}
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array
{
$associationData = [];
$idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);
foreach ($idChunks as $idChunk) {
try {
$batchInput = new \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId();
$batchInput->setInputs(array_map(function ($id) {
$publicObjectId = new \HubSpot\Client\Crm\Associations\Model\PublicObjectId();
$publicObjectId->setId($id);
return $publicObjectId;
}, $idChunk));
$associatedObjectsData = $this
->getNewInstance()
->crm()
->associations()
->batchApi()
->read($fromObject, $toObject, $batchInput);
if ($associatedObjectsData instanceof \HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti) {
foreach ($associatedObjectsData->getResults() as $association) {
$from = $association->getFrom()->getId();
$toAssociations = $association->getTo();
if (! empty($toAssociations)) {
$associationData[$from] = array_map(function ($item) {
return $item->getId();
}, $toAssociations);
}
}
}
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to fetch associations', [
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => $e->getMessage(),
]);
}
}
return $associationData;
}
/**
* @throws \Exception
*/
private function getNoteAssociationType(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'note_to_deal',
NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it
NoteObject::Account => 'note_to_company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
/**
* @throws \Exception
*/
private function getNoteObject(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'deal',
NoteObject::Lead, NoteObject::Contact => 'contact',
NoteObject::Account => 'company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
public function addAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/create";
return $this->makeRequest($endpoint, 'POST', $payload);
}
public function removeAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/archive";
return $this->makeRequest($endpoint, 'POST', $payload);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.034242023,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: master","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"Editor for custom.log","depth":4,"bounds":{"left":0.5475399,"top":0.0726257,"width":0.44082448,"height":0.9066241},"on_screen":true,"role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.007978723,"height":0.0},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"60","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.010305851,"height":0.0},"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.00731383,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.006981383,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Component\\Utility\\Service\\ProviderRateLimiter;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n private ProviderRateLimiter $rateLimiter;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager,\n ProviderRateLimiter $rateLimiter,\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n $this->rateLimiter = $rateLimiter;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Single entry point for every HubSpot API call. Enforces the per-portal\n * rate limit configured in the rate_limits table (morphed to the current\n * Configuration) and reacts to a real 429 from HubSpot by translating it\n * into a RateLimitException carrying Retry-After.\n *\n * Wrap any outbound HubSpot call (SDK or raw HTTP) like:\n *\n * $this->executeRequest(fn () => $this->getNewInstance()->crm()->...);\n *\n * @template T\n * @param callable(): T $apiCall\n * @return T\n *\n * @throws RateLimitException\n */\n private function executeRequest(callable $apiCall)\n {\n if (! $this->rateLimiter->canMakeRequest($this->config)) {\n $retryAfter = $this->rateLimiter->requestAvailableIn($this->config);\n\n $this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n ]);\n\n throw new RateLimitException(\n 'Hubspot rate limit reached for configuration ' . $this->config->getId(),\n $retryAfter,\n );\n }\n\n $this->rateLimiter->incrementRequestCount($this->config);\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n private function isHubspotRateLimit(Throwable $e): bool\n {\n return method_exists($e, 'getCode') && (int) $e->getCode() === 429;\n }\n\n private function parseRetryAfter(Throwable $e): int\n {\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n return 10;\n }\n\n public function getMinimumApiVersion(): string\n {\n return self::MIN_API_VERSION;\n }\n\n public function getInstance(): Factory\n {\n return new Factory([\n 'key' => $this->accessToken,\n 'oauth2' => true,\n 'base_url' => $this->baseUrl,\n ]);\n }\n\n public function getNewInstance(): Discovery\n {\n return \\HubSpot\\Factory::createWithAccessToken($this->accessToken);\n }\n\n /**\n * Secondly and daily limits for Hubspot API\n *\n * Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)\n * Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds\n * Daily: 250,000 | 500,000 | 1,000,000\n *\n * Official documentation states: The search endpoints are rate limited to five requests per second.\n * Since with 5 RPS were still hitting secondly rate limits we lowered it to 4\n */\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array\n {\n $total = 0;\n $lastId = null;\n $rows = [];\n foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {\n $rows[] = $row;\n }\n\n return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n return $this->paginationService->getPaginatedDataGenerator(\n $this,\n $payload,\n $type,\n $offset,\n $total,\n $lastRecordId\n );\n }\n\n /**\n * @throws DealApiException\n * @throws CrmException\n */\n public function getOpportunityById(string $crmId, array $fields): array\n {\n try {\n $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n ));\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n $response = $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n $response = $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n\n $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // \"110\"\n $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // \"109\"\n $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // \"10000\"\n $body = json_decode((string) $response->getBody(), true);\n\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));\n\n return $response;\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Component\\Utility\\Service\\ProviderRateLimiter;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n private ProviderRateLimiter $rateLimiter;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager,\n ProviderRateLimiter $rateLimiter,\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n $this->rateLimiter = $rateLimiter;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Single entry point for every HubSpot API call. Enforces the per-portal\n * rate limit configured in the rate_limits table (morphed to the current\n * Configuration) and reacts to a real 429 from HubSpot by translating it\n * into a RateLimitException carrying Retry-After.\n *\n * Wrap any outbound HubSpot call (SDK or raw HTTP) like:\n *\n * $this->executeRequest(fn () => $this->getNewInstance()->crm()->...);\n *\n * @template T\n * @param callable(): T $apiCall\n * @return T\n *\n * @throws RateLimitException\n */\n private function executeRequest(callable $apiCall)\n {\n if (! $this->rateLimiter->canMakeRequest($this->config)) {\n $retryAfter = $this->rateLimiter->requestAvailableIn($this->config);\n\n $this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n ]);\n\n throw new RateLimitException(\n 'Hubspot rate limit reached for configuration ' . $this->config->getId(),\n $retryAfter,\n );\n }\n\n $this->rateLimiter->incrementRequestCount($this->config);\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n private function isHubspotRateLimit(Throwable $e): bool\n {\n return method_exists($e, 'getCode') && (int) $e->getCode() === 429;\n }\n\n private function parseRetryAfter(Throwable $e): int\n {\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n return 10;\n }\n\n public function getMinimumApiVersion(): string\n {\n return self::MIN_API_VERSION;\n }\n\n public function getInstance(): Factory\n {\n return new Factory([\n 'key' => $this->accessToken,\n 'oauth2' => true,\n 'base_url' => $this->baseUrl,\n ]);\n }\n\n public function getNewInstance(): Discovery\n {\n return \\HubSpot\\Factory::createWithAccessToken($this->accessToken);\n }\n\n /**\n * Secondly and daily limits for Hubspot API\n *\n * Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)\n * Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds\n * Daily: 250,000 | 500,000 | 1,000,000\n *\n * Official documentation states: The search endpoints are rate limited to five requests per second.\n * Since with 5 RPS were still hitting secondly rate limits we lowered it to 4\n */\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array\n {\n $total = 0;\n $lastId = null;\n $rows = [];\n foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {\n $rows[] = $row;\n }\n\n return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n return $this->paginationService->getPaginatedDataGenerator(\n $this,\n $payload,\n $type,\n $offset,\n $total,\n $lastRecordId\n );\n }\n\n /**\n * @throws DealApiException\n * @throws CrmException\n */\n public function getOpportunityById(string $crmId, array $fields): array\n {\n try {\n $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n ));\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n $response = $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n $response = $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n\n $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // \"110\"\n $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // \"109\"\n $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // \"10000\"\n $body = json_decode((string) $response->getBody(), true);\n\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));\n\n return $response;\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-188086479456983350
|
6378757184196315236
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
Editor for custom.log
Sync Changes
Hide This Notification
Code changed:
Hide
2
60
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot;
use HubSpot\Client\Crm\Deals\ApiException as DealApiException;
use HubSpot\Client\Crm\Contacts\ApiException as ContactApiException;
use HubSpot\Client\Crm\Companies\ApiException as CompanyApiException;
use HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations as ContactsWithAssociations;
use HubSpot\Client\Crm\Companies\Model\SimplePublicObjectWithAssociations as CompaniesWithAssociations;
use HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations as DealWithAssociations;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectInput;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectWithAssociations as ObjectWithAssociations;
use HubSpot\Client\Crm\Pipelines\Model\Error;
use HubSpot\Client\Crm\Pipelines\Model\PipelineStage;
use HubSpot\Client\Crm\Properties\Model\Property;
use HubSpot\Discovery\Discovery;
use Jiminny\Component\Utility\Service\ProviderRateLimiter;
use Jiminny\Exceptions\CrmException;
use Jiminny\Exceptions\RateLimitException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Jobs\Crm\NoteObject;
use Jiminny\Models\Crm\Field;
use Jiminny\Services\Crm\BaseClient;
use Jiminny\Services\Crm\Hubspot\DTO\Response\Owner;
use Jiminny\Services\SocialAccountService;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use SevenShores\Hubspot\Factory;
use SevenShores\Hubspot\Http\Response;
use Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService;
use Throwable;
/**
* @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
* @param callable(): T $apiCall
* @return T
*
* @throws RateLimitException
*/
private function executeRequest(callable $apiCall)
{
if (! $this->rateLimiter->canMakeRequest($this->config)) {
$retryAfter = $this->rateLimiter->requestAvailableIn($this->config);
$this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
]);
throw new RateLimitException(
'Hubspot rate limit reached for configuration ' . $this->config->getId(),
$retryAfter,
);
}
$this->rateLimiter->incrementRequestCount($this->config);
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function isHubspotRateLimit(Throwable $e): bool
{
return method_exists($e, 'getCode') && (int) $e->getCode() === 429;
}
private function parseRetryAfter(Throwable $e): int
{
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
return 10;
}
public function getMinimumApiVersion(): string
{
return self::MIN_API_VERSION;
}
public function getInstance(): Factory
{
return new Factory([
'key' => $this->accessToken,
'oauth2' => true,
'base_url' => $this->baseUrl,
]);
}
public function getNewInstance(): Discovery
{
return \HubSpot\Factory::createWithAccessToken($this->accessToken);
}
/**
* Secondly and daily limits for Hubspot API
*
* Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)
* Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds
* Daily: 250,000 | 500,000 | 1,000,000
*
* Official documentation states: The search endpoints are rate limited to five requests per second.
* Since with 5 RPS were still hitting secondly rate limits we lowered it to 4
*/
public function getPaginatedData(array $payload, string $type, int $offset = 0): array
{
$total = 0;
$lastId = null;
$rows = [];
foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {
$rows[] = $row;
}
return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
return $this->paginationService->getPaginatedDataGenerator(
$this,
$payload,
$type,
$offset,
$total,
$lastRecordId
);
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
$deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
));
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $batchConfig['api']->read($batchReadRequest);
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
$response = $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
$response = $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
$max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // "110"
$remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // "109"
$interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // "10000"
$body = json_decode((string) $response->getBody(), true);
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));
return $response;
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function createMeeting(array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings';
return $this->makeRequest($endpoint, 'POST', $payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function updateMeeting(string $meetingId, array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings/' . $meetingId;
return $this->makeRequest($endpoint, 'PATCH', $payload);
}
/**
* @throws \Exception
*/
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string {
try {
$noteInput = new SimplePublicObjectInput([
'properties' => [
'hs_note_body' => $body,
'hubspot_owner_id' => $ownerId,
'hs_timestamp' => $timestamp,
],
]);
// Create note
$note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);
$this->getNewInstance()->crm()->objects()->associationsApi()->create(
'note',
$note->getId(),
$this->getNoteObject($noteObject),
$objectId,
$this->getNoteAssociationType($noteObject),
);
return $note->getId();
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to create note', [
'objectId' => $objectId,
'noteObject' => $noteObject->getObjectType(),
'reason' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return null;
}
public function updateEngagement(string $objectId, array $engagement, array $metadata): void
{
$this->getInstance()->engagements()->update($objectId, $engagement, $metadata);
}
public function getEngagementData(string $engagementId): array
{
$engagement = $this->getInstance()->engagements()->get($engagementId);
return $engagement->toArray();
}
public function createEngagement(array $engagement, array $associations, array $metadata): Response
{
return $this->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
}
public function isUnauthorizedException(\Exception $e): bool
{
// Check for specific HubSpot API exception types first
if ($e instanceof BadRequest) {
// BadRequest can contain 401 status codes
return $e->getCode() === 401;
}
// Check for HTTP client exceptions with status codes
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$response = $e->getResponse();
if ($response !== null) {
return $response->getStatusCode() === 401;
}
}
// Check for Guzzle HTTP exceptions
if ($e instanceof \GuzzleHttp\Exception\ClientException) {
return $e->getCode() === 401;
}
// Fallback to string matching as last resort, but be more specific
$message = strtolower($e->getMessage());
return str_contains($message, '401 unauthorized') ||
str_contains($message, 'http 401') ||
str_contains($message, 'status code 401') ||
(preg_match('/\b401\b/', $message) && str_contains($message, 'unauthorized'));
}
/**
* Validates and refreshes the access token if needed before API requests.
* This ensures long-running processes don't fail due to token expiration.
*
* @throws SocialAccountTokenInvalidException
*/
public function ensureValidToken(): void
{
if ($this->oauthAccount === null) {
return;
}
$newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);
if ($newToken !== null) {
$this->accessToken = $newToken;
}
}
public function getConfig()
{
return $this->config;
}
// returns only active (archived=false)
public function getOwners(): array
{
return $this->getNewInstance()->crm()->owners()->getAll();
}
/**
* @param bool $archived
*
* @return array<Owner>|[]
*/
public function getOwnersArchived(bool $archived = true): array
{
$endpoint = '/crm/v3/owners';
$queryParams = [
'archived' => $archived ? 'true' : 'false',
];
$queryString = http_build_query($queryParams);
$owners = [];
try {
$response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);
$responseData = $response?->toArray();
foreach ($responseData['results'] as $result) {
try {
$owners[] = Owner::create($result);
} catch (Throwable $e) {
$this->log->error('[HubSpot] Failed to process owner data', [
'result' => $result,
'error' => $e->getMessage(),
]);
continue;
}
}
} catch (Throwable $e) {
$this->log->error('HubSpot] Failed to fetch owners', [
'archived' => $archived,
'error' => $e->getMessage(),
]);
return [];
}
return $owners;
}
public function getMeeting(string $engagementId): ObjectWithAssociations
{
return $this->getNewInstance()->crm()->objects()->basicApi()
->getById('meeting', $engagementId, null, 'contact,company,deal');
}
public function deleteEngagement(string $engagementId): void
{
$this->getInstance()->engagements()->delete((int) $engagementId);
}
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array
{
$associationData = [];
$idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);
foreach ($idChunks as $idChunk) {
try {
$batchInput = new \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId();
$batchInput->setInputs(array_map(function ($id) {
$publicObjectId = new \HubSpot\Client\Crm\Associations\Model\PublicObjectId();
$publicObjectId->setId($id);
return $publicObjectId;
}, $idChunk));
$associatedObjectsData = $this
->getNewInstance()
->crm()
->associations()
->batchApi()
->read($fromObject, $toObject, $batchInput);
if ($associatedObjectsData instanceof \HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti) {
foreach ($associatedObjectsData->getResults() as $association) {
$from = $association->getFrom()->getId();
$toAssociations = $association->getTo();
if (! empty($toAssociations)) {
$associationData[$from] = array_map(function ($item) {
return $item->getId();
}, $toAssociations);
}
}
}
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to fetch associations', [
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => $e->getMessage(),
]);
}
}
return $associationData;
}
/**
* @throws \Exception
*/
private function getNoteAssociationType(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'note_to_deal',
NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it
NoteObject::Account => 'note_to_company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
/**
* @throws \Exception
*/
private function getNoteObject(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'deal',
NoteObject::Lead, NoteObject::Contact => 'contact',
NoteObject::Account => 'company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
public function addAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/create";
return $this->makeRequest($endpoint, 'POST', $payload);
}
public function removeAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/archive";
return $this->makeRequest($endpoint, 'POST', $payload);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
2870
|
NULL
|
NULL
|
NULL
|
|
2871
|
NULL
|
0
|
2026-05-07T11:44:25.222220+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778154265222_m1.jpg...
|
PhpStorm
|
faVsco.js – Hubspot/Client.php
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
Editor for custom.log
Sync Changes
Hide This Notification
Code changed:
Hide
2
60
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot;
use HubSpot\Client\Crm\Deals\ApiException as DealApiException;
use HubSpot\Client\Crm\Contacts\ApiException as ContactApiException;
use HubSpot\Client\Crm\Companies\ApiException as CompanyApiException;
use HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations as ContactsWithAssociations;
use HubSpot\Client\Crm\Companies\Model\SimplePublicObjectWithAssociations as CompaniesWithAssociations;
use HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations as DealWithAssociations;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectInput;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectWithAssociations as ObjectWithAssociations;
use HubSpot\Client\Crm\Pipelines\Model\Error;
use HubSpot\Client\Crm\Pipelines\Model\PipelineStage;
use HubSpot\Client\Crm\Properties\Model\Property;
use HubSpot\Discovery\Discovery;
use Jiminny\Component\Utility\Service\ProviderRateLimiter;
use Jiminny\Exceptions\CrmException;
use Jiminny\Exceptions\RateLimitException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Jobs\Crm\NoteObject;
use Jiminny\Models\Crm\Field;
use Jiminny\Services\Crm\BaseClient;
use Jiminny\Services\Crm\Hubspot\DTO\Response\Owner;
use Jiminny\Services\SocialAccountService;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use SevenShores\Hubspot\Factory;
use SevenShores\Hubspot\Http\Response;
use Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService;
use Throwable;
/**
* @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
* @param callable(): T $apiCall
* @return T
*
* @throws RateLimitException
*/
private function executeRequest(callable $apiCall)
{
if (! $this->rateLimiter->canMakeRequest($this->config)) {
$retryAfter = $this->rateLimiter->requestAvailableIn($this->config);
$this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
]);
throw new RateLimitException(
'Hubspot rate limit reached for configuration ' . $this->config->getId(),
$retryAfter,
);
}
$this->rateLimiter->incrementRequestCount($this->config);
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function isHubspotRateLimit(Throwable $e): bool
{
return method_exists($e, 'getCode') && (int) $e->getCode() === 429;
}
private function parseRetryAfter(Throwable $e): int
{
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
return 10;
}
public function getMinimumApiVersion(): string
{
return self::MIN_API_VERSION;
}
public function getInstance(): Factory
{
return new Factory([
'key' => $this->accessToken,
'oauth2' => true,
'base_url' => $this->baseUrl,
]);
}
public function getNewInstance(): Discovery
{
return \HubSpot\Factory::createWithAccessToken($this->accessToken);
}
/**
* Secondly and daily limits for Hubspot API
*
* Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)
* Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds
* Daily: 250,000 | 500,000 | 1,000,000
*
* Official documentation states: The search endpoints are rate limited to five requests per second.
* Since with 5 RPS were still hitting secondly rate limits we lowered it to 4
*/
public function getPaginatedData(array $payload, string $type, int $offset = 0): array
{
$total = 0;
$lastId = null;
$rows = [];
foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {
$rows[] = $row;
}
return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
return $this->paginationService->getPaginatedDataGenerator(
$this,
$payload,
$type,
$offset,
$total,
$lastRecordId
);
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
$deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
));
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $batchConfig['api']->read($batchReadRequest);
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
$response = $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
$response = $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
$max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // "110"
$remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // "109"
$interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // "10000"
$body = json_decode((string) $response->getBody(), true);
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));
return $response;
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function createMeeting(array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings';
return $this->makeRequest($endpoint, 'POST', $payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function updateMeeting(string $meetingId, array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings/' . $meetingId;
return $this->makeRequest($endpoint, 'PATCH', $payload);
}
/**
* @throws \Exception
*/
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string {
try {
$noteInput = new SimplePublicObjectInput([
'properties' => [
'hs_note_body' => $body,
'hubspot_owner_id' => $ownerId,
'hs_timestamp' => $timestamp,
],
]);
// Create note
$note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);
$this->getNewInstance()->crm()->objects()->associationsApi()->create(
'note',
$note->getId(),
$this->getNoteObject($noteObject),
$objectId,
$this->getNoteAssociationType($noteObject),
);
return $note->getId();
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to create note', [
'objectId' => $objectId,
'noteObject' => $noteObject->getObjectType(),
'reason' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return null;
}
public function updateEngagement(string $objectId, array $engagement, array $metadata): void
{
$this->getInstance()->engagements()->update($objectId, $engagement, $metadata);
}
public function getEngagementData(string $engagementId): array
{
$engagement = $this->getInstance()->engagements()->get($engagementId);
return $engagement->toArray();
}
public function createEngagement(array $engagement, array $associations, array $metadata): Response
{
return $this->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
}
public function isUnauthorizedException(\Exception $e): bool
{
// Check for specific HubSpot API exception types first
if ($e instanceof BadRequest) {
// BadRequest can contain 401 status codes
return $e->getCode() === 401;
}
// Check for HTTP client exceptions with status codes
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$response = $e->getResponse();
if ($response !== null) {
return $response->getStatusCode() === 401;
}
}
// Check for Guzzle HTTP exceptions
if ($e instanceof \GuzzleHttp\Exception\ClientException) {
return $e->getCode() === 401;
}
// Fallback to string matching as last resort, but be more specific
$message = strtolower($e->getMessage());
return str_contains($message, '401 unauthorized') ||
str_contains($message, 'http 401') ||
str_contains($message, 'status code 401') ||
(preg_match('/\b401\b/', $message) && str_contains($message, 'unauthorized'));
}
/**
* Validates and refreshes the access token if needed before API requests.
* This ensures long-running processes don't fail due to token expiration.
*
* @throws SocialAccountTokenInvalidException
*/
public function ensureValidToken(): void
{
if ($this->oauthAccount === null) {
return;
}
$newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);
if ($newToken !== null) {
$this->accessToken = $newToken;
}
}
public function getConfig()
{
return $this->config;
}
// returns only active (archived=false)
public function getOwners(): array
{
return $this->getNewInstance()->crm()->owners()->getAll();
}
/**
* @param bool $archived
*
* @return array<Owner>|[]
*/
public function getOwnersArchived(bool $archived = true): array
{
$endpoint = '/crm/v3/owners';
$queryParams = [
'archived' => $archived ? 'true' : 'false',
];
$queryString = http_build_query($queryParams);
$owners = [];
try {
$response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);
$responseData = $response?->toArray();
foreach ($responseData['results'] as $result) {
try {
$owners[] = Owner::create($result);
} catch (Throwable $e) {
$this->log->error('[HubSpot] Failed to process owner data', [
'result' => $result,
'error' => $e->getMessage(),
]);
continue;
}
}
} catch (Throwable $e) {
$this->log->error('HubSpot] Failed to fetch owners', [
'archived' => $archived,
'error' => $e->getMessage(),
]);
return [];
}
return $owners;
}
public function getMeeting(string $engagementId): ObjectWithAssociations
{
return $this->getNewInstance()->crm()->objects()->basicApi()
->getById('meeting', $engagementId, null, 'contact,company,deal');
}
public function deleteEngagement(string $engagementId): void
{
$this->getInstance()->engagements()->delete((int) $engagementId);
}
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array
{
$associationData = [];
$idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);
foreach ($idChunks as $idChunk) {
try {
$batchInput = new \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId();
$batchInput->setInputs(array_map(function ($id) {
$publicObjectId = new \HubSpot\Client\Crm\Associations\Model\PublicObjectId();
$publicObjectId->setId($id);
return $publicObjectId;
}, $idChunk));
$associatedObjectsData = $this
->getNewInstance()
->crm()
->associations()
->batchApi()
->read($fromObject, $toObject, $batchInput);
if ($associatedObjectsData instanceof \HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti) {
foreach ($associatedObjectsData->getResults() as $association) {
$from = $association->getFrom()->getId();
$toAssociations = $association->getTo();
if (! empty($toAssociations)) {
$associationData[$from] = array_map(function ($item) {
return $item->getId();
}, $toAssociations);
}
}
}
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to fetch associations', [
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => $e->getMessage(),
]);
}
}
return $associationData;
}
/**
* @throws \Exception
*/
private function getNoteAssociationType(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'note_to_deal',
NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it
NoteObject::Account => 'note_to_company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
/**
* @throws \Exception
*/
private function getNoteObject(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'deal',
NoteObject::Lead, NoteObject::Contact => 'contact',
NoteObject::Account => 'company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
public function addAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/create";
return $this->makeRequest($endpoint, 'POST', $payload);
}
public function removeAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/archive";
return $this->makeRequest($endpoint, 'POST', $payload);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"on_screen":true,"help_text":"Git Branch: master","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"Editor for custom.log","depth":4,"on_screen":true,"role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.016666668,"height":0.02111111},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"60","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.021527778,"height":0.02111111},"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.015277778,"height":0.025555555},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.014583333,"height":0.025555555},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Component\\Utility\\Service\\ProviderRateLimiter;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n private ProviderRateLimiter $rateLimiter;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager,\n ProviderRateLimiter $rateLimiter,\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n $this->rateLimiter = $rateLimiter;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Single entry point for every HubSpot API call. Enforces the per-portal\n * rate limit configured in the rate_limits table (morphed to the current\n * Configuration) and reacts to a real 429 from HubSpot by translating it\n * into a RateLimitException carrying Retry-After.\n *\n * Wrap any outbound HubSpot call (SDK or raw HTTP) like:\n *\n * $this->executeRequest(fn () => $this->getNewInstance()->crm()->...);\n *\n * @template T\n * @param callable(): T $apiCall\n * @return T\n *\n * @throws RateLimitException\n */\n private function executeRequest(callable $apiCall)\n {\n if (! $this->rateLimiter->canMakeRequest($this->config)) {\n $retryAfter = $this->rateLimiter->requestAvailableIn($this->config);\n\n $this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n ]);\n\n throw new RateLimitException(\n 'Hubspot rate limit reached for configuration ' . $this->config->getId(),\n $retryAfter,\n );\n }\n\n $this->rateLimiter->incrementRequestCount($this->config);\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n private function isHubspotRateLimit(Throwable $e): bool\n {\n return method_exists($e, 'getCode') && (int) $e->getCode() === 429;\n }\n\n private function parseRetryAfter(Throwable $e): int\n {\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n return 10;\n }\n\n public function getMinimumApiVersion(): string\n {\n return self::MIN_API_VERSION;\n }\n\n public function getInstance(): Factory\n {\n return new Factory([\n 'key' => $this->accessToken,\n 'oauth2' => true,\n 'base_url' => $this->baseUrl,\n ]);\n }\n\n public function getNewInstance(): Discovery\n {\n return \\HubSpot\\Factory::createWithAccessToken($this->accessToken);\n }\n\n /**\n * Secondly and daily limits for Hubspot API\n *\n * Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)\n * Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds\n * Daily: 250,000 | 500,000 | 1,000,000\n *\n * Official documentation states: The search endpoints are rate limited to five requests per second.\n * Since with 5 RPS were still hitting secondly rate limits we lowered it to 4\n */\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array\n {\n $total = 0;\n $lastId = null;\n $rows = [];\n foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {\n $rows[] = $row;\n }\n\n return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n return $this->paginationService->getPaginatedDataGenerator(\n $this,\n $payload,\n $type,\n $offset,\n $total,\n $lastRecordId\n );\n }\n\n /**\n * @throws DealApiException\n * @throws CrmException\n */\n public function getOpportunityById(string $crmId, array $fields): array\n {\n try {\n $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n ));\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n $response = $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n $response = $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n\n $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // \"110\"\n $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // \"109\"\n $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // \"10000\"\n $body = json_decode((string) $response->getBody(), true);\n\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));\n\n return $response;\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Component\\Utility\\Service\\ProviderRateLimiter;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n private ProviderRateLimiter $rateLimiter;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager,\n ProviderRateLimiter $rateLimiter,\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n $this->rateLimiter = $rateLimiter;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Single entry point for every HubSpot API call. Enforces the per-portal\n * rate limit configured in the rate_limits table (morphed to the current\n * Configuration) and reacts to a real 429 from HubSpot by translating it\n * into a RateLimitException carrying Retry-After.\n *\n * Wrap any outbound HubSpot call (SDK or raw HTTP) like:\n *\n * $this->executeRequest(fn () => $this->getNewInstance()->crm()->...);\n *\n * @template T\n * @param callable(): T $apiCall\n * @return T\n *\n * @throws RateLimitException\n */\n private function executeRequest(callable $apiCall)\n {\n if (! $this->rateLimiter->canMakeRequest($this->config)) {\n $retryAfter = $this->rateLimiter->requestAvailableIn($this->config);\n\n $this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n ]);\n\n throw new RateLimitException(\n 'Hubspot rate limit reached for configuration ' . $this->config->getId(),\n $retryAfter,\n );\n }\n\n $this->rateLimiter->incrementRequestCount($this->config);\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n private function isHubspotRateLimit(Throwable $e): bool\n {\n return method_exists($e, 'getCode') && (int) $e->getCode() === 429;\n }\n\n private function parseRetryAfter(Throwable $e): int\n {\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n return 10;\n }\n\n public function getMinimumApiVersion(): string\n {\n return self::MIN_API_VERSION;\n }\n\n public function getInstance(): Factory\n {\n return new Factory([\n 'key' => $this->accessToken,\n 'oauth2' => true,\n 'base_url' => $this->baseUrl,\n ]);\n }\n\n public function getNewInstance(): Discovery\n {\n return \\HubSpot\\Factory::createWithAccessToken($this->accessToken);\n }\n\n /**\n * Secondly and daily limits for Hubspot API\n *\n * Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)\n * Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds\n * Daily: 250,000 | 500,000 | 1,000,000\n *\n * Official documentation states: The search endpoints are rate limited to five requests per second.\n * Since with 5 RPS were still hitting secondly rate limits we lowered it to 4\n */\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array\n {\n $total = 0;\n $lastId = null;\n $rows = [];\n foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {\n $rows[] = $row;\n }\n\n return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n return $this->paginationService->getPaginatedDataGenerator(\n $this,\n $payload,\n $type,\n $offset,\n $total,\n $lastRecordId\n );\n }\n\n /**\n * @throws DealApiException\n * @throws CrmException\n */\n public function getOpportunityById(string $crmId, array $fields): array\n {\n try {\n $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n ));\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n $response = $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n $response = $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n\n $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // \"110\"\n $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // \"109\"\n $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // \"10000\"\n $body = json_decode((string) $response->getBody(), true);\n\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));\n\n return $response;\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-188086479456983350
|
6378757184196315236
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
Editor for custom.log
Sync Changes
Hide This Notification
Code changed:
Hide
2
60
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot;
use HubSpot\Client\Crm\Deals\ApiException as DealApiException;
use HubSpot\Client\Crm\Contacts\ApiException as ContactApiException;
use HubSpot\Client\Crm\Companies\ApiException as CompanyApiException;
use HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations as ContactsWithAssociations;
use HubSpot\Client\Crm\Companies\Model\SimplePublicObjectWithAssociations as CompaniesWithAssociations;
use HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations as DealWithAssociations;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectInput;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectWithAssociations as ObjectWithAssociations;
use HubSpot\Client\Crm\Pipelines\Model\Error;
use HubSpot\Client\Crm\Pipelines\Model\PipelineStage;
use HubSpot\Client\Crm\Properties\Model\Property;
use HubSpot\Discovery\Discovery;
use Jiminny\Component\Utility\Service\ProviderRateLimiter;
use Jiminny\Exceptions\CrmException;
use Jiminny\Exceptions\RateLimitException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Jobs\Crm\NoteObject;
use Jiminny\Models\Crm\Field;
use Jiminny\Services\Crm\BaseClient;
use Jiminny\Services\Crm\Hubspot\DTO\Response\Owner;
use Jiminny\Services\SocialAccountService;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use SevenShores\Hubspot\Factory;
use SevenShores\Hubspot\Http\Response;
use Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService;
use Throwable;
/**
* @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
* @param callable(): T $apiCall
* @return T
*
* @throws RateLimitException
*/
private function executeRequest(callable $apiCall)
{
if (! $this->rateLimiter->canMakeRequest($this->config)) {
$retryAfter = $this->rateLimiter->requestAvailableIn($this->config);
$this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
]);
throw new RateLimitException(
'Hubspot rate limit reached for configuration ' . $this->config->getId(),
$retryAfter,
);
}
$this->rateLimiter->incrementRequestCount($this->config);
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function isHubspotRateLimit(Throwable $e): bool
{
return method_exists($e, 'getCode') && (int) $e->getCode() === 429;
}
private function parseRetryAfter(Throwable $e): int
{
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
return 10;
}
public function getMinimumApiVersion(): string
{
return self::MIN_API_VERSION;
}
public function getInstance(): Factory
{
return new Factory([
'key' => $this->accessToken,
'oauth2' => true,
'base_url' => $this->baseUrl,
]);
}
public function getNewInstance(): Discovery
{
return \HubSpot\Factory::createWithAccessToken($this->accessToken);
}
/**
* Secondly and daily limits for Hubspot API
*
* Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)
* Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds
* Daily: 250,000 | 500,000 | 1,000,000
*
* Official documentation states: The search endpoints are rate limited to five requests per second.
* Since with 5 RPS were still hitting secondly rate limits we lowered it to 4
*/
public function getPaginatedData(array $payload, string $type, int $offset = 0): array
{
$total = 0;
$lastId = null;
$rows = [];
foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {
$rows[] = $row;
}
return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
return $this->paginationService->getPaginatedDataGenerator(
$this,
$payload,
$type,
$offset,
$total,
$lastRecordId
);
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
$deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
));
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $batchConfig['api']->read($batchReadRequest);
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
$response = $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
$response = $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
$max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // "110"
$remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // "109"
$interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // "10000"
$body = json_decode((string) $response->getBody(), true);
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));
return $response;
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function createMeeting(array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings';
return $this->makeRequest($endpoint, 'POST', $payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function updateMeeting(string $meetingId, array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings/' . $meetingId;
return $this->makeRequest($endpoint, 'PATCH', $payload);
}
/**
* @throws \Exception
*/
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string {
try {
$noteInput = new SimplePublicObjectInput([
'properties' => [
'hs_note_body' => $body,
'hubspot_owner_id' => $ownerId,
'hs_timestamp' => $timestamp,
],
]);
// Create note
$note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);
$this->getNewInstance()->crm()->objects()->associationsApi()->create(
'note',
$note->getId(),
$this->getNoteObject($noteObject),
$objectId,
$this->getNoteAssociationType($noteObject),
);
return $note->getId();
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to create note', [
'objectId' => $objectId,
'noteObject' => $noteObject->getObjectType(),
'reason' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return null;
}
public function updateEngagement(string $objectId, array $engagement, array $metadata): void
{
$this->getInstance()->engagements()->update($objectId, $engagement, $metadata);
}
public function getEngagementData(string $engagementId): array
{
$engagement = $this->getInstance()->engagements()->get($engagementId);
return $engagement->toArray();
}
public function createEngagement(array $engagement, array $associations, array $metadata): Response
{
return $this->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
}
public function isUnauthorizedException(\Exception $e): bool
{
// Check for specific HubSpot API exception types first
if ($e instanceof BadRequest) {
// BadRequest can contain 401 status codes
return $e->getCode() === 401;
}
// Check for HTTP client exceptions with status codes
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$response = $e->getResponse();
if ($response !== null) {
return $response->getStatusCode() === 401;
}
}
// Check for Guzzle HTTP exceptions
if ($e instanceof \GuzzleHttp\Exception\ClientException) {
return $e->getCode() === 401;
}
// Fallback to string matching as last resort, but be more specific
$message = strtolower($e->getMessage());
return str_contains($message, '401 unauthorized') ||
str_contains($message, 'http 401') ||
str_contains($message, 'status code 401') ||
(preg_match('/\b401\b/', $message) && str_contains($message, 'unauthorized'));
}
/**
* Validates and refreshes the access token if needed before API requests.
* This ensures long-running processes don't fail due to token expiration.
*
* @throws SocialAccountTokenInvalidException
*/
public function ensureValidToken(): void
{
if ($this->oauthAccount === null) {
return;
}
$newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);
if ($newToken !== null) {
$this->accessToken = $newToken;
}
}
public function getConfig()
{
return $this->config;
}
// returns only active (archived=false)
public function getOwners(): array
{
return $this->getNewInstance()->crm()->owners()->getAll();
}
/**
* @param bool $archived
*
* @return array<Owner>|[]
*/
public function getOwnersArchived(bool $archived = true): array
{
$endpoint = '/crm/v3/owners';
$queryParams = [
'archived' => $archived ? 'true' : 'false',
];
$queryString = http_build_query($queryParams);
$owners = [];
try {
$response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);
$responseData = $response?->toArray();
foreach ($responseData['results'] as $result) {
try {
$owners[] = Owner::create($result);
} catch (Throwable $e) {
$this->log->error('[HubSpot] Failed to process owner data', [
'result' => $result,
'error' => $e->getMessage(),
]);
continue;
}
}
} catch (Throwable $e) {
$this->log->error('HubSpot] Failed to fetch owners', [
'archived' => $archived,
'error' => $e->getMessage(),
]);
return [];
}
return $owners;
}
public function getMeeting(string $engagementId): ObjectWithAssociations
{
return $this->getNewInstance()->crm()->objects()->basicApi()
->getById('meeting', $engagementId, null, 'contact,company,deal');
}
public function deleteEngagement(string $engagementId): void
{
$this->getInstance()->engagements()->delete((int) $engagementId);
}
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array
{
$associationData = [];
$idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);
foreach ($idChunks as $idChunk) {
try {
$batchInput = new \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId();
$batchInput->setInputs(array_map(function ($id) {
$publicObjectId = new \HubSpot\Client\Crm\Associations\Model\PublicObjectId();
$publicObjectId->setId($id);
return $publicObjectId;
}, $idChunk));
$associatedObjectsData = $this
->getNewInstance()
->crm()
->associations()
->batchApi()
->read($fromObject, $toObject, $batchInput);
if ($associatedObjectsData instanceof \HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti) {
foreach ($associatedObjectsData->getResults() as $association) {
$from = $association->getFrom()->getId();
$toAssociations = $association->getTo();
if (! empty($toAssociations)) {
$associationData[$from] = array_map(function ($item) {
return $item->getId();
}, $toAssociations);
}
}
}
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to fetch associations', [
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => $e->getMessage(),
]);
}
}
return $associationData;
}
/**
* @throws \Exception
*/
private function getNoteAssociationType(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'note_to_deal',
NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it
NoteObject::Account => 'note_to_company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
/**
* @throws \Exception
*/
private function getNoteObject(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'deal',
NoteObject::Lead, NoteObject::Contact => 'contact',
NoteObject::Account => 'company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
public function addAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/create";
return $this->makeRequest($endpoint, 'POST', $payload);
}
public function removeAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/archive";
return $this->makeRequest($endpoint, 'POST', $payload);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
2868
|
NULL
|
NULL
|
NULL
|
|
2870
|
NULL
|
0
|
2026-05-07T11:44:22.040879+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778154262040_m2.jpg...
|
PhpStorm
|
faVsco.js – Hubspot/Client.php
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
Editor for custom.log
Sync Changes
Hide This Notification
Code changed:
Hide
2
60
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot;
use HubSpot\Client\Crm\Deals\ApiException as DealApiException;
use HubSpot\Client\Crm\Contacts\ApiException as ContactApiException;
use HubSpot\Client\Crm\Companies\ApiException as CompanyApiException;
use HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations as ContactsWithAssociations;
use HubSpot\Client\Crm\Companies\Model\SimplePublicObjectWithAssociations as CompaniesWithAssociations;
use HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations as DealWithAssociations;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectInput;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectWithAssociations as ObjectWithAssociations;
use HubSpot\Client\Crm\Pipelines\Model\Error;
use HubSpot\Client\Crm\Pipelines\Model\PipelineStage;
use HubSpot\Client\Crm\Properties\Model\Property;
use HubSpot\Discovery\Discovery;
use Jiminny\Component\Utility\Service\ProviderRateLimiter;
use Jiminny\Exceptions\CrmException;
use Jiminny\Exceptions\RateLimitException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Jobs\Crm\NoteObject;
use Jiminny\Models\Crm\Field;
use Jiminny\Services\Crm\BaseClient;
use Jiminny\Services\Crm\Hubspot\DTO\Response\Owner;
use Jiminny\Services\SocialAccountService;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use SevenShores\Hubspot\Factory;
use SevenShores\Hubspot\Http\Response;
use Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService;
use Throwable;
/**
* @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
* @param callable(): T $apiCall
* @return T
*
* @throws RateLimitException
*/
private function executeRequest(callable $apiCall)
{
if (! $this->rateLimiter->canMakeRequest($this->config)) {
$retryAfter = $this->rateLimiter->requestAvailableIn($this->config);
$this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
]);
throw new RateLimitException(
'Hubspot rate limit reached for configuration ' . $this->config->getId(),
$retryAfter,
);
}
$this->rateLimiter->incrementRequestCount($this->config);
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function isHubspotRateLimit(Throwable $e): bool
{
return method_exists($e, 'getCode') && (int) $e->getCode() === 429;
}
private function parseRetryAfter(Throwable $e): int
{
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
return 10;
}
public function getMinimumApiVersion(): string
{
return self::MIN_API_VERSION;
}
public function getInstance(): Factory
{
return new Factory([
'key' => $this->accessToken,
'oauth2' => true,
'base_url' => $this->baseUrl,
]);
}
public function getNewInstance(): Discovery
{
return \HubSpot\Factory::createWithAccessToken($this->accessToken);
}
/**
* Secondly and daily limits for Hubspot API
*
* Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)
* Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds
* Daily: 250,000 | 500,000 | 1,000,000
*
* Official documentation states: The search endpoints are rate limited to five requests per second.
* Since with 5 RPS were still hitting secondly rate limits we lowered it to 4
*/
public function getPaginatedData(array $payload, string $type, int $offset = 0): array
{
$total = 0;
$lastId = null;
$rows = [];
foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {
$rows[] = $row;
}
return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
return $this->paginationService->getPaginatedDataGenerator(
$this,
$payload,
$type,
$offset,
$total,
$lastRecordId
);
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
$deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
));
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $batchConfig['api']->read($batchReadRequest);
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
$response = $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
$response = $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
$max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // "110"
$remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // "109"
$interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // "10000"
$body = json_decode((string) $response->getBody(), true);
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));
return $response;
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function createMeeting(array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings';
return $this->makeRequest($endpoint, 'POST', $payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function updateMeeting(string $meetingId, array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings/' . $meetingId;
return $this->makeRequest($endpoint, 'PATCH', $payload);
}
/**
* @throws \Exception
*/
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string {
try {
$noteInput = new SimplePublicObjectInput([
'properties' => [
'hs_note_body' => $body,
'hubspot_owner_id' => $ownerId,
'hs_timestamp' => $timestamp,
],
]);
// Create note
$note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);
$this->getNewInstance()->crm()->objects()->associationsApi()->create(
'note',
$note->getId(),
$this->getNoteObject($noteObject),
$objectId,
$this->getNoteAssociationType($noteObject),
);
return $note->getId();
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to create note', [
'objectId' => $objectId,
'noteObject' => $noteObject->getObjectType(),
'reason' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return null;
}
public function updateEngagement(string $objectId, array $engagement, array $metadata): void
{
$this->getInstance()->engagements()->update($objectId, $engagement, $metadata);
}
public function getEngagementData(string $engagementId): array
{
$engagement = $this->getInstance()->engagements()->get($engagementId);
return $engagement->toArray();
}
public function createEngagement(array $engagement, array $associations, array $metadata): Response
{
return $this->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
}
public function isUnauthorizedException(\Exception $e): bool
{
// Check for specific HubSpot API exception types first
if ($e instanceof BadRequest) {
// BadRequest can contain 401 status codes
return $e->getCode() === 401;
}
// Check for HTTP client exceptions with status codes
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$response = $e->getResponse();
if ($response !== null) {
return $response->getStatusCode() === 401;
}
}
// Check for Guzzle HTTP exceptions
if ($e instanceof \GuzzleHttp\Exception\ClientException) {
return $e->getCode() === 401;
}
// Fallback to string matching as last resort, but be more specific
$message = strtolower($e->getMessage());
return str_contains($message, '401 unauthorized') ||
str_contains($message, 'http 401') ||
str_contains($message, 'status code 401') ||
(preg_match('/\b401\b/', $message) && str_contains($message, 'unauthorized'));
}
/**
* Validates and refreshes the access token if needed before API requests.
* This ensures long-running processes don't fail due to token expiration.
*
* @throws SocialAccountTokenInvalidException
*/
public function ensureValidToken(): void
{
if ($this->oauthAccount === null) {
return;
}
$newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);
if ($newToken !== null) {
$this->accessToken = $newToken;
}
}
public function getConfig()
{
return $this->config;
}
// returns only active (archived=false)
public function getOwners(): array
{
return $this->getNewInstance()->crm()->owners()->getAll();
}
/**
* @param bool $archived
*
* @return array<Owner>|[]
*/
public function getOwnersArchived(bool $archived = true): array
{
$endpoint = '/crm/v3/owners';
$queryParams = [
'archived' => $archived ? 'true' : 'false',
];
$queryString = http_build_query($queryParams);
$owners = [];
try {
$response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);
$responseData = $response?->toArray();
foreach ($responseData['results'] as $result) {
try {
$owners[] = Owner::create($result);
} catch (Throwable $e) {
$this->log->error('[HubSpot] Failed to process owner data', [
'result' => $result,
'error' => $e->getMessage(),
]);
continue;
}
}
} catch (Throwable $e) {
$this->log->error('HubSpot] Failed to fetch owners', [
'archived' => $archived,
'error' => $e->getMessage(),
]);
return [];
}
return $owners;
}
public function getMeeting(string $engagementId): ObjectWithAssociations
{
return $this->getNewInstance()->crm()->objects()->basicApi()
->getById('meeting', $engagementId, null, 'contact,company,deal');
}
public function deleteEngagement(string $engagementId): void
{
$this->getInstance()->engagements()->delete((int) $engagementId);
}
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array
{
$associationData = [];
$idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);
foreach ($idChunks as $idChunk) {
try {
$batchInput = new \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId();
$batchInput->setInputs(array_map(function ($id) {
$publicObjectId = new \HubSpot\Client\Crm\Associations\Model\PublicObjectId();
$publicObjectId->setId($id);
return $publicObjectId;
}, $idChunk));
$associatedObjectsData = $this
->getNewInstance()
->crm()
->associations()
->batchApi()
->read($fromObject, $toObject, $batchInput);
if ($associatedObjectsData instanceof \HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti) {
foreach ($associatedObjectsData->getResults() as $association) {
$from = $association->getFrom()->getId();
$toAssociations = $association->getTo();
if (! empty($toAssociations)) {
$associationData[$from] = array_map(function ($item) {
return $item->getId();
}, $toAssociations);
}
}
}
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to fetch associations', [
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => $e->getMessage(),
]);
}
}
return $associationData;
}
/**
* @throws \Exception
*/
private function getNoteAssociationType(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'note_to_deal',
NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it
NoteObject::Account => 'note_to_company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
/**
* @throws \Exception
*/
private function getNoteObject(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'deal',
NoteObject::Lead, NoteObject::Contact => 'contact',
NoteObject::Account => 'company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
public function addAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/create";
return $this->makeRequest($endpoint, 'POST', $payload);
}
public function removeAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/archive";
return $this->makeRequest($endpoint, 'POST', $payload);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.034242023,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: master","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"Editor for custom.log","depth":4,"bounds":{"left":0.5475399,"top":0.0726257,"width":0.44082448,"height":0.9066241},"on_screen":true,"role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.007978723,"height":0.0},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"60","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.010305851,"height":0.0},"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.00731383,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.006981383,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Component\\Utility\\Service\\ProviderRateLimiter;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n private ProviderRateLimiter $rateLimiter;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager,\n ProviderRateLimiter $rateLimiter,\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n $this->rateLimiter = $rateLimiter;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Single entry point for every HubSpot API call. Enforces the per-portal\n * rate limit configured in the rate_limits table (morphed to the current\n * Configuration) and reacts to a real 429 from HubSpot by translating it\n * into a RateLimitException carrying Retry-After.\n *\n * Wrap any outbound HubSpot call (SDK or raw HTTP) like:\n *\n * $this->executeRequest(fn () => $this->getNewInstance()->crm()->...);\n *\n * @template T\n * @param callable(): T $apiCall\n * @return T\n *\n * @throws RateLimitException\n */\n private function executeRequest(callable $apiCall)\n {\n if (! $this->rateLimiter->canMakeRequest($this->config)) {\n $retryAfter = $this->rateLimiter->requestAvailableIn($this->config);\n\n $this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n ]);\n\n throw new RateLimitException(\n 'Hubspot rate limit reached for configuration ' . $this->config->getId(),\n $retryAfter,\n );\n }\n\n $this->rateLimiter->incrementRequestCount($this->config);\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n private function isHubspotRateLimit(Throwable $e): bool\n {\n return method_exists($e, 'getCode') && (int) $e->getCode() === 429;\n }\n\n private function parseRetryAfter(Throwable $e): int\n {\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n return 10;\n }\n\n public function getMinimumApiVersion(): string\n {\n return self::MIN_API_VERSION;\n }\n\n public function getInstance(): Factory\n {\n return new Factory([\n 'key' => $this->accessToken,\n 'oauth2' => true,\n 'base_url' => $this->baseUrl,\n ]);\n }\n\n public function getNewInstance(): Discovery\n {\n return \\HubSpot\\Factory::createWithAccessToken($this->accessToken);\n }\n\n /**\n * Secondly and daily limits for Hubspot API\n *\n * Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)\n * Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds\n * Daily: 250,000 | 500,000 | 1,000,000\n *\n * Official documentation states: The search endpoints are rate limited to five requests per second.\n * Since with 5 RPS were still hitting secondly rate limits we lowered it to 4\n */\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array\n {\n $total = 0;\n $lastId = null;\n $rows = [];\n foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {\n $rows[] = $row;\n }\n\n return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n return $this->paginationService->getPaginatedDataGenerator(\n $this,\n $payload,\n $type,\n $offset,\n $total,\n $lastRecordId\n );\n }\n\n /**\n * @throws DealApiException\n * @throws CrmException\n */\n public function getOpportunityById(string $crmId, array $fields): array\n {\n try {\n $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n ));\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n $response = $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n $response = $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n\n $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // \"110\"\n $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // \"109\"\n $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // \"10000\"\n $body = json_decode((string) $response->getBody(), true);\n\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));\n\n return $response;\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Component\\Utility\\Service\\ProviderRateLimiter;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n private ProviderRateLimiter $rateLimiter;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager,\n ProviderRateLimiter $rateLimiter,\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n $this->rateLimiter = $rateLimiter;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Single entry point for every HubSpot API call. Enforces the per-portal\n * rate limit configured in the rate_limits table (morphed to the current\n * Configuration) and reacts to a real 429 from HubSpot by translating it\n * into a RateLimitException carrying Retry-After.\n *\n * Wrap any outbound HubSpot call (SDK or raw HTTP) like:\n *\n * $this->executeRequest(fn () => $this->getNewInstance()->crm()->...);\n *\n * @template T\n * @param callable(): T $apiCall\n * @return T\n *\n * @throws RateLimitException\n */\n private function executeRequest(callable $apiCall)\n {\n if (! $this->rateLimiter->canMakeRequest($this->config)) {\n $retryAfter = $this->rateLimiter->requestAvailableIn($this->config);\n\n $this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n ]);\n\n throw new RateLimitException(\n 'Hubspot rate limit reached for configuration ' . $this->config->getId(),\n $retryAfter,\n );\n }\n\n $this->rateLimiter->incrementRequestCount($this->config);\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n private function isHubspotRateLimit(Throwable $e): bool\n {\n return method_exists($e, 'getCode') && (int) $e->getCode() === 429;\n }\n\n private function parseRetryAfter(Throwable $e): int\n {\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n return 10;\n }\n\n public function getMinimumApiVersion(): string\n {\n return self::MIN_API_VERSION;\n }\n\n public function getInstance(): Factory\n {\n return new Factory([\n 'key' => $this->accessToken,\n 'oauth2' => true,\n 'base_url' => $this->baseUrl,\n ]);\n }\n\n public function getNewInstance(): Discovery\n {\n return \\HubSpot\\Factory::createWithAccessToken($this->accessToken);\n }\n\n /**\n * Secondly and daily limits for Hubspot API\n *\n * Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)\n * Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds\n * Daily: 250,000 | 500,000 | 1,000,000\n *\n * Official documentation states: The search endpoints are rate limited to five requests per second.\n * Since with 5 RPS were still hitting secondly rate limits we lowered it to 4\n */\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array\n {\n $total = 0;\n $lastId = null;\n $rows = [];\n foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {\n $rows[] = $row;\n }\n\n return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n return $this->paginationService->getPaginatedDataGenerator(\n $this,\n $payload,\n $type,\n $offset,\n $total,\n $lastRecordId\n );\n }\n\n /**\n * @throws DealApiException\n * @throws CrmException\n */\n public function getOpportunityById(string $crmId, array $fields): array\n {\n try {\n $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n ));\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n $response = $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n $response = $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n\n $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // \"110\"\n $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // \"109\"\n $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // \"10000\"\n $body = json_decode((string) $response->getBody(), true);\n\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));\n\n return $response;\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-188086479456983350
|
6378757184196315236
|
visual_change
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
Editor for custom.log
Sync Changes
Hide This Notification
Code changed:
Hide
2
60
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot;
use HubSpot\Client\Crm\Deals\ApiException as DealApiException;
use HubSpot\Client\Crm\Contacts\ApiException as ContactApiException;
use HubSpot\Client\Crm\Companies\ApiException as CompanyApiException;
use HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations as ContactsWithAssociations;
use HubSpot\Client\Crm\Companies\Model\SimplePublicObjectWithAssociations as CompaniesWithAssociations;
use HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations as DealWithAssociations;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectInput;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectWithAssociations as ObjectWithAssociations;
use HubSpot\Client\Crm\Pipelines\Model\Error;
use HubSpot\Client\Crm\Pipelines\Model\PipelineStage;
use HubSpot\Client\Crm\Properties\Model\Property;
use HubSpot\Discovery\Discovery;
use Jiminny\Component\Utility\Service\ProviderRateLimiter;
use Jiminny\Exceptions\CrmException;
use Jiminny\Exceptions\RateLimitException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Jobs\Crm\NoteObject;
use Jiminny\Models\Crm\Field;
use Jiminny\Services\Crm\BaseClient;
use Jiminny\Services\Crm\Hubspot\DTO\Response\Owner;
use Jiminny\Services\SocialAccountService;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use SevenShores\Hubspot\Factory;
use SevenShores\Hubspot\Http\Response;
use Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService;
use Throwable;
/**
* @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
* @param callable(): T $apiCall
* @return T
*
* @throws RateLimitException
*/
private function executeRequest(callable $apiCall)
{
if (! $this->rateLimiter->canMakeRequest($this->config)) {
$retryAfter = $this->rateLimiter->requestAvailableIn($this->config);
$this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
]);
throw new RateLimitException(
'Hubspot rate limit reached for configuration ' . $this->config->getId(),
$retryAfter,
);
}
$this->rateLimiter->incrementRequestCount($this->config);
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function isHubspotRateLimit(Throwable $e): bool
{
return method_exists($e, 'getCode') && (int) $e->getCode() === 429;
}
private function parseRetryAfter(Throwable $e): int
{
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
return 10;
}
public function getMinimumApiVersion(): string
{
return self::MIN_API_VERSION;
}
public function getInstance(): Factory
{
return new Factory([
'key' => $this->accessToken,
'oauth2' => true,
'base_url' => $this->baseUrl,
]);
}
public function getNewInstance(): Discovery
{
return \HubSpot\Factory::createWithAccessToken($this->accessToken);
}
/**
* Secondly and daily limits for Hubspot API
*
* Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)
* Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds
* Daily: 250,000 | 500,000 | 1,000,000
*
* Official documentation states: The search endpoints are rate limited to five requests per second.
* Since with 5 RPS were still hitting secondly rate limits we lowered it to 4
*/
public function getPaginatedData(array $payload, string $type, int $offset = 0): array
{
$total = 0;
$lastId = null;
$rows = [];
foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {
$rows[] = $row;
}
return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
return $this->paginationService->getPaginatedDataGenerator(
$this,
$payload,
$type,
$offset,
$total,
$lastRecordId
);
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
$deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
));
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $batchConfig['api']->read($batchReadRequest);
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
$response = $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
$response = $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
$max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // "110"
$remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // "109"
$interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // "10000"
$body = json_decode((string) $response->getBody(), true);
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));
return $response;
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function createMeeting(array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings';
return $this->makeRequest($endpoint, 'POST', $payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function updateMeeting(string $meetingId, array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings/' . $meetingId;
return $this->makeRequest($endpoint, 'PATCH', $payload);
}
/**
* @throws \Exception
*/
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string {
try {
$noteInput = new SimplePublicObjectInput([
'properties' => [
'hs_note_body' => $body,
'hubspot_owner_id' => $ownerId,
'hs_timestamp' => $timestamp,
],
]);
// Create note
$note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);
$this->getNewInstance()->crm()->objects()->associationsApi()->create(
'note',
$note->getId(),
$this->getNoteObject($noteObject),
$objectId,
$this->getNoteAssociationType($noteObject),
);
return $note->getId();
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to create note', [
'objectId' => $objectId,
'noteObject' => $noteObject->getObjectType(),
'reason' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return null;
}
public function updateEngagement(string $objectId, array $engagement, array $metadata): void
{
$this->getInstance()->engagements()->update($objectId, $engagement, $metadata);
}
public function getEngagementData(string $engagementId): array
{
$engagement = $this->getInstance()->engagements()->get($engagementId);
return $engagement->toArray();
}
public function createEngagement(array $engagement, array $associations, array $metadata): Response
{
return $this->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
}
public function isUnauthorizedException(\Exception $e): bool
{
// Check for specific HubSpot API exception types first
if ($e instanceof BadRequest) {
// BadRequest can contain 401 status codes
return $e->getCode() === 401;
}
// Check for HTTP client exceptions with status codes
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$response = $e->getResponse();
if ($response !== null) {
return $response->getStatusCode() === 401;
}
}
// Check for Guzzle HTTP exceptions
if ($e instanceof \GuzzleHttp\Exception\ClientException) {
return $e->getCode() === 401;
}
// Fallback to string matching as last resort, but be more specific
$message = strtolower($e->getMessage());
return str_contains($message, '401 unauthorized') ||
str_contains($message, 'http 401') ||
str_contains($message, 'status code 401') ||
(preg_match('/\b401\b/', $message) && str_contains($message, 'unauthorized'));
}
/**
* Validates and refreshes the access token if needed before API requests.
* This ensures long-running processes don't fail due to token expiration.
*
* @throws SocialAccountTokenInvalidException
*/
public function ensureValidToken(): void
{
if ($this->oauthAccount === null) {
return;
}
$newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);
if ($newToken !== null) {
$this->accessToken = $newToken;
}
}
public function getConfig()
{
return $this->config;
}
// returns only active (archived=false)
public function getOwners(): array
{
return $this->getNewInstance()->crm()->owners()->getAll();
}
/**
* @param bool $archived
*
* @return array<Owner>|[]
*/
public function getOwnersArchived(bool $archived = true): array
{
$endpoint = '/crm/v3/owners';
$queryParams = [
'archived' => $archived ? 'true' : 'false',
];
$queryString = http_build_query($queryParams);
$owners = [];
try {
$response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);
$responseData = $response?->toArray();
foreach ($responseData['results'] as $result) {
try {
$owners[] = Owner::create($result);
} catch (Throwable $e) {
$this->log->error('[HubSpot] Failed to process owner data', [
'result' => $result,
'error' => $e->getMessage(),
]);
continue;
}
}
} catch (Throwable $e) {
$this->log->error('HubSpot] Failed to fetch owners', [
'archived' => $archived,
'error' => $e->getMessage(),
]);
return [];
}
return $owners;
}
public function getMeeting(string $engagementId): ObjectWithAssociations
{
return $this->getNewInstance()->crm()->objects()->basicApi()
->getById('meeting', $engagementId, null, 'contact,company,deal');
}
public function deleteEngagement(string $engagementId): void
{
$this->getInstance()->engagements()->delete((int) $engagementId);
}
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array
{
$associationData = [];
$idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);
foreach ($idChunks as $idChunk) {
try {
$batchInput = new \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId();
$batchInput->setInputs(array_map(function ($id) {
$publicObjectId = new \HubSpot\Client\Crm\Associations\Model\PublicObjectId();
$publicObjectId->setId($id);
return $publicObjectId;
}, $idChunk));
$associatedObjectsData = $this
->getNewInstance()
->crm()
->associations()
->batchApi()
->read($fromObject, $toObject, $batchInput);
if ($associatedObjectsData instanceof \HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti) {
foreach ($associatedObjectsData->getResults() as $association) {
$from = $association->getFrom()->getId();
$toAssociations = $association->getTo();
if (! empty($toAssociations)) {
$associationData[$from] = array_map(function ($item) {
return $item->getId();
}, $toAssociations);
}
}
}
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to fetch associations', [
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => $e->getMessage(),
]);
}
}
return $associationData;
}
/**
* @throws \Exception
*/
private function getNoteAssociationType(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'note_to_deal',
NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it
NoteObject::Account => 'note_to_company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
/**
* @throws \Exception
*/
private function getNoteObject(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'deal',
NoteObject::Lead, NoteObject::Contact => 'contact',
NoteObject::Account => 'company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
public function addAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/create";
return $this->makeRequest($endpoint, 'POST', $payload);
}
public function removeAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/archive";
return $this->makeRequest($endpoint, 'POST', $payload);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
2869
|
114
|
50
|
2026-05-07T11:43:49.771263+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778154229771_m2.jpg...
|
PhpStorm
|
faVsco.js – custom.log
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
Editor for custom.log
Sync Changes
Hide This Notification
Code changed:
Hide
2
60
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot;
use HubSpot\Client\Crm\Deals\ApiException as DealApiException;
use HubSpot\Client\Crm\Contacts\ApiException as ContactApiException;
use HubSpot\Client\Crm\Companies\ApiException as CompanyApiException;
use HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations as ContactsWithAssociations;
use HubSpot\Client\Crm\Companies\Model\SimplePublicObjectWithAssociations as CompaniesWithAssociations;
use HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations as DealWithAssociations;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectInput;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectWithAssociations as ObjectWithAssociations;
use HubSpot\Client\Crm\Pipelines\Model\Error;
use HubSpot\Client\Crm\Pipelines\Model\PipelineStage;
use HubSpot\Client\Crm\Properties\Model\Property;
use HubSpot\Discovery\Discovery;
use Jiminny\Component\Utility\Service\ProviderRateLimiter;
use Jiminny\Exceptions\CrmException;
use Jiminny\Exceptions\RateLimitException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Jobs\Crm\NoteObject;
use Jiminny\Models\Crm\Field;
use Jiminny\Services\Crm\BaseClient;
use Jiminny\Services\Crm\Hubspot\DTO\Response\Owner;
use Jiminny\Services\SocialAccountService;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use SevenShores\Hubspot\Factory;
use SevenShores\Hubspot\Http\Response;
use Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService;
use Throwable;
/**
* @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
* @param callable(): T $apiCall
* @return T
*
* @throws RateLimitException
*/
private function executeRequest(callable $apiCall)
{
if (! $this->rateLimiter->canMakeRequest($this->config)) {
$retryAfter = $this->rateLimiter->requestAvailableIn($this->config);
$this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
]);
throw new RateLimitException(
'Hubspot rate limit reached for configuration ' . $this->config->getId(),
$retryAfter,
);
}
$this->rateLimiter->incrementRequestCount($this->config);
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function isHubspotRateLimit(Throwable $e): bool
{
return method_exists($e, 'getCode') && (int) $e->getCode() === 429;
}
private function parseRetryAfter(Throwable $e): int
{
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
return 10;
}
public function getMinimumApiVersion(): string
{
return self::MIN_API_VERSION;
}
public function getInstance(): Factory
{
return new Factory([
'key' => $this->accessToken,
'oauth2' => true,
'base_url' => $this->baseUrl,
]);
}
public function getNewInstance(): Discovery
{
return \HubSpot\Factory::createWithAccessToken($this->accessToken);
}
/**
* Secondly and daily limits for Hubspot API
*
* Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)
* Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds
* Daily: 250,000 | 500,000 | 1,000,000
*
* Official documentation states: The search endpoints are rate limited to five requests per second.
* Since with 5 RPS were still hitting secondly rate limits we lowered it to 4
*/
public function getPaginatedData(array $payload, string $type, int $offset = 0): array
{
$total = 0;
$lastId = null;
$rows = [];
foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {
$rows[] = $row;
}
return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
return $this->paginationService->getPaginatedDataGenerator(
$this,
$payload,
$type,
$offset,
$total,
$lastRecordId
);
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
$deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
));
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $batchConfig['api']->read($batchReadRequest);
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
$response = $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
$response = $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
$max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // "110"
$remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // "109"
$interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // "10000"
$body = json_decode((string) $response->getBody(), true);
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));
return $response;
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function createMeeting(array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings';
return $this->makeRequest($endpoint, 'POST', $payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function updateMeeting(string $meetingId, array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings/' . $meetingId;
return $this->makeRequest($endpoint, 'PATCH', $payload);
}
/**
* @throws \Exception
*/
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string {
try {
$noteInput = new SimplePublicObjectInput([
'properties' => [
'hs_note_body' => $body,
'hubspot_owner_id' => $ownerId,
'hs_timestamp' => $timestamp,
],
]);
// Create note
$note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);
$this->getNewInstance()->crm()->objects()->associationsApi()->create(
'note',
$note->getId(),
$this->getNoteObject($noteObject),
$objectId,
$this->getNoteAssociationType($noteObject),
);
return $note->getId();
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to create note', [
'objectId' => $objectId,
'noteObject' => $noteObject->getObjectType(),
'reason' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return null;
}
public function updateEngagement(string $objectId, array $engagement, array $metadata): void
{
$this->getInstance()->engagements()->update($objectId, $engagement, $metadata);
}
public function getEngagementData(string $engagementId): array
{
$engagement = $this->getInstance()->engagements()->get($engagementId);
return $engagement->toArray();
}
public function createEngagement(array $engagement, array $associations, array $metadata): Response
{
return $this->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
}
public function isUnauthorizedException(\Exception $e): bool
{
// Check for specific HubSpot API exception types first
if ($e instanceof BadRequest) {
// BadRequest can contain 401 status codes
return $e->getCode() === 401;
}
// Check for HTTP client exceptions with status codes
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$response = $e->getResponse();
if ($response !== null) {
return $response->getStatusCode() === 401;
}
}
// Check for Guzzle HTTP exceptions
if ($e instanceof \GuzzleHttp\Exception\ClientException) {
return $e->getCode() === 401;
}
// Fallback to string matching as last resort, but be more specific
$message = strtolower($e->getMessage());
return str_contains($message, '401 unauthorized') ||
str_contains($message, 'http 401') ||
str_contains($message, 'status code 401') ||
(preg_match('/\b401\b/', $message) && str_contains($message, 'unauthorized'));
}
/**
* Validates and refreshes the access token if needed before API requests.
* This ensures long-running processes don't fail due to token expiration.
*
* @throws SocialAccountTokenInvalidException
*/
public function ensureValidToken(): void
{
if ($this->oauthAccount === null) {
return;
}
$newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);
if ($newToken !== null) {
$this->accessToken = $newToken;
}
}
public function getConfig()
{
return $this->config;
}
// returns only active (archived=false)
public function getOwners(): array
{
return $this->getNewInstance()->crm()->owners()->getAll();
}
/**
* @param bool $archived
*
* @return array<Owner>|[]
*/
public function getOwnersArchived(bool $archived = true): array
{
$endpoint = '/crm/v3/owners';
$queryParams = [
'archived' => $archived ? 'true' : 'false',
];
$queryString = http_build_query($queryParams);
$owners = [];
try {
$response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);
$responseData = $response?->toArray();
foreach ($responseData['results'] as $result) {
try {
$owners[] = Owner::create($result);
} catch (Throwable $e) {
$this->log->error('[HubSpot] Failed to process owner data', [
'result' => $result,
'error' => $e->getMessage(),
]);
continue;
}
}
} catch (Throwable $e) {
$this->log->error('HubSpot] Failed to fetch owners', [
'archived' => $archived,
'error' => $e->getMessage(),
]);
return [];
}
return $owners;
}
public function getMeeting(string $engagementId): ObjectWithAssociations
{
return $this->getNewInstance()->crm()->objects()->basicApi()
->getById('meeting', $engagementId, null, 'contact,company,deal');
}
public function deleteEngagement(string $engagementId): void
{
$this->getInstance()->engagements()->delete((int) $engagementId);
}
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array
{
$associationData = [];
$idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);
foreach ($idChunks as $idChunk) {
try {
$batchInput = new \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId();
$batchInput->setInputs(array_map(function ($id) {
$publicObjectId = new \HubSpot\Client\Crm\Associations\Model\PublicObjectId();
$publicObjectId->setId($id);
return $publicObjectId;
}, $idChunk));
$associatedObjectsData = $this
->getNewInstance()
->crm()
->associations()
->batchApi()
->read($fromObject, $toObject, $batchInput);
if ($associatedObjectsData instanceof \HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti) {
foreach ($associatedObjectsData->getResults() as $association) {
$from = $association->getFrom()->getId();
$toAssociations = $association->getTo();
if (! empty($toAssociations)) {
$associationData[$from] = array_map(function ($item) {
return $item->getId();
}, $toAssociations);
}
}
}
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to fetch associations', [
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => $e->getMessage(),
]);
}
}
return $associationData;
}
/**
* @throws \Exception
*/
private function getNoteAssociationType(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'note_to_deal',
NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it
NoteObject::Account => 'note_to_company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
/**
* @throws \Exception
*/
private function getNoteObject(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'deal',
NoteObject::Lead, NoteObject::Contact => 'contact',
NoteObject::Account => 'company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
public function addAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/create";
return $this->makeRequest($endpoint, 'POST', $payload);
}
public function removeAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/archive";
return $this->makeRequest($endpoint, 'POST', $payload);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.034242023,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: master","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"Editor for custom.log","depth":4,"bounds":{"left":0.5475399,"top":0.0726257,"width":0.44082448,"height":0.9066241},"on_screen":true,"role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.007978723,"height":0.0},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"60","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.010305851,"height":0.0},"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.00731383,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.006981383,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Component\\Utility\\Service\\ProviderRateLimiter;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n private ProviderRateLimiter $rateLimiter;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager,\n ProviderRateLimiter $rateLimiter,\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n $this->rateLimiter = $rateLimiter;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Single entry point for every HubSpot API call. Enforces the per-portal\n * rate limit configured in the rate_limits table (morphed to the current\n * Configuration) and reacts to a real 429 from HubSpot by translating it\n * into a RateLimitException carrying Retry-After.\n *\n * Wrap any outbound HubSpot call (SDK or raw HTTP) like:\n *\n * $this->executeRequest(fn () => $this->getNewInstance()->crm()->...);\n *\n * @template T\n * @param callable(): T $apiCall\n * @return T\n *\n * @throws RateLimitException\n */\n private function executeRequest(callable $apiCall)\n {\n if (! $this->rateLimiter->canMakeRequest($this->config)) {\n $retryAfter = $this->rateLimiter->requestAvailableIn($this->config);\n\n $this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n ]);\n\n throw new RateLimitException(\n 'Hubspot rate limit reached for configuration ' . $this->config->getId(),\n $retryAfter,\n );\n }\n\n $this->rateLimiter->incrementRequestCount($this->config);\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n private function isHubspotRateLimit(Throwable $e): bool\n {\n return method_exists($e, 'getCode') && (int) $e->getCode() === 429;\n }\n\n private function parseRetryAfter(Throwable $e): int\n {\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n return 10;\n }\n\n public function getMinimumApiVersion(): string\n {\n return self::MIN_API_VERSION;\n }\n\n public function getInstance(): Factory\n {\n return new Factory([\n 'key' => $this->accessToken,\n 'oauth2' => true,\n 'base_url' => $this->baseUrl,\n ]);\n }\n\n public function getNewInstance(): Discovery\n {\n return \\HubSpot\\Factory::createWithAccessToken($this->accessToken);\n }\n\n /**\n * Secondly and daily limits for Hubspot API\n *\n * Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)\n * Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds\n * Daily: 250,000 | 500,000 | 1,000,000\n *\n * Official documentation states: The search endpoints are rate limited to five requests per second.\n * Since with 5 RPS were still hitting secondly rate limits we lowered it to 4\n */\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array\n {\n $total = 0;\n $lastId = null;\n $rows = [];\n foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {\n $rows[] = $row;\n }\n\n return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n return $this->paginationService->getPaginatedDataGenerator(\n $this,\n $payload,\n $type,\n $offset,\n $total,\n $lastRecordId\n );\n }\n\n /**\n * @throws DealApiException\n * @throws CrmException\n */\n public function getOpportunityById(string $crmId, array $fields): array\n {\n try {\n $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n ));\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n $response = $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n $response = $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n\n $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // \"110\"\n $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // \"109\"\n $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // \"10000\"\n $body = json_decode((string) $response->getBody(), true);\n\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));\n\n return $response;\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Component\\Utility\\Service\\ProviderRateLimiter;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n private ProviderRateLimiter $rateLimiter;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager,\n ProviderRateLimiter $rateLimiter,\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n $this->rateLimiter = $rateLimiter;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Single entry point for every HubSpot API call. Enforces the per-portal\n * rate limit configured in the rate_limits table (morphed to the current\n * Configuration) and reacts to a real 429 from HubSpot by translating it\n * into a RateLimitException carrying Retry-After.\n *\n * Wrap any outbound HubSpot call (SDK or raw HTTP) like:\n *\n * $this->executeRequest(fn () => $this->getNewInstance()->crm()->...);\n *\n * @template T\n * @param callable(): T $apiCall\n * @return T\n *\n * @throws RateLimitException\n */\n private function executeRequest(callable $apiCall)\n {\n if (! $this->rateLimiter->canMakeRequest($this->config)) {\n $retryAfter = $this->rateLimiter->requestAvailableIn($this->config);\n\n $this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n ]);\n\n throw new RateLimitException(\n 'Hubspot rate limit reached for configuration ' . $this->config->getId(),\n $retryAfter,\n );\n }\n\n $this->rateLimiter->incrementRequestCount($this->config);\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n private function isHubspotRateLimit(Throwable $e): bool\n {\n return method_exists($e, 'getCode') && (int) $e->getCode() === 429;\n }\n\n private function parseRetryAfter(Throwable $e): int\n {\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n return 10;\n }\n\n public function getMinimumApiVersion(): string\n {\n return self::MIN_API_VERSION;\n }\n\n public function getInstance(): Factory\n {\n return new Factory([\n 'key' => $this->accessToken,\n 'oauth2' => true,\n 'base_url' => $this->baseUrl,\n ]);\n }\n\n public function getNewInstance(): Discovery\n {\n return \\HubSpot\\Factory::createWithAccessToken($this->accessToken);\n }\n\n /**\n * Secondly and daily limits for Hubspot API\n *\n * Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)\n * Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds\n * Daily: 250,000 | 500,000 | 1,000,000\n *\n * Official documentation states: The search endpoints are rate limited to five requests per second.\n * Since with 5 RPS were still hitting secondly rate limits we lowered it to 4\n */\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array\n {\n $total = 0;\n $lastId = null;\n $rows = [];\n foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {\n $rows[] = $row;\n }\n\n return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n return $this->paginationService->getPaginatedDataGenerator(\n $this,\n $payload,\n $type,\n $offset,\n $total,\n $lastRecordId\n );\n }\n\n /**\n * @throws DealApiException\n * @throws CrmException\n */\n public function getOpportunityById(string $crmId, array $fields): array\n {\n try {\n $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n ));\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n $response = $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n $response = $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n\n $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // \"110\"\n $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // \"109\"\n $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // \"10000\"\n $body = json_decode((string) $response->getBody(), true);\n\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));\n\n return $response;\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-188086479456983350
|
6378757184196315236
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
Editor for custom.log
Sync Changes
Hide This Notification
Code changed:
Hide
2
60
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot;
use HubSpot\Client\Crm\Deals\ApiException as DealApiException;
use HubSpot\Client\Crm\Contacts\ApiException as ContactApiException;
use HubSpot\Client\Crm\Companies\ApiException as CompanyApiException;
use HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations as ContactsWithAssociations;
use HubSpot\Client\Crm\Companies\Model\SimplePublicObjectWithAssociations as CompaniesWithAssociations;
use HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations as DealWithAssociations;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectInput;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectWithAssociations as ObjectWithAssociations;
use HubSpot\Client\Crm\Pipelines\Model\Error;
use HubSpot\Client\Crm\Pipelines\Model\PipelineStage;
use HubSpot\Client\Crm\Properties\Model\Property;
use HubSpot\Discovery\Discovery;
use Jiminny\Component\Utility\Service\ProviderRateLimiter;
use Jiminny\Exceptions\CrmException;
use Jiminny\Exceptions\RateLimitException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Jobs\Crm\NoteObject;
use Jiminny\Models\Crm\Field;
use Jiminny\Services\Crm\BaseClient;
use Jiminny\Services\Crm\Hubspot\DTO\Response\Owner;
use Jiminny\Services\SocialAccountService;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use SevenShores\Hubspot\Factory;
use SevenShores\Hubspot\Http\Response;
use Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService;
use Throwable;
/**
* @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
* @param callable(): T $apiCall
* @return T
*
* @throws RateLimitException
*/
private function executeRequest(callable $apiCall)
{
if (! $this->rateLimiter->canMakeRequest($this->config)) {
$retryAfter = $this->rateLimiter->requestAvailableIn($this->config);
$this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
]);
throw new RateLimitException(
'Hubspot rate limit reached for configuration ' . $this->config->getId(),
$retryAfter,
);
}
$this->rateLimiter->incrementRequestCount($this->config);
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function isHubspotRateLimit(Throwable $e): bool
{
return method_exists($e, 'getCode') && (int) $e->getCode() === 429;
}
private function parseRetryAfter(Throwable $e): int
{
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
return 10;
}
public function getMinimumApiVersion(): string
{
return self::MIN_API_VERSION;
}
public function getInstance(): Factory
{
return new Factory([
'key' => $this->accessToken,
'oauth2' => true,
'base_url' => $this->baseUrl,
]);
}
public function getNewInstance(): Discovery
{
return \HubSpot\Factory::createWithAccessToken($this->accessToken);
}
/**
* Secondly and daily limits for Hubspot API
*
* Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)
* Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds
* Daily: 250,000 | 500,000 | 1,000,000
*
* Official documentation states: The search endpoints are rate limited to five requests per second.
* Since with 5 RPS were still hitting secondly rate limits we lowered it to 4
*/
public function getPaginatedData(array $payload, string $type, int $offset = 0): array
{
$total = 0;
$lastId = null;
$rows = [];
foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {
$rows[] = $row;
}
return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
return $this->paginationService->getPaginatedDataGenerator(
$this,
$payload,
$type,
$offset,
$total,
$lastRecordId
);
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
$deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
));
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $batchConfig['api']->read($batchReadRequest);
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
$response = $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
$response = $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
$max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // "110"
$remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // "109"
$interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // "10000"
$body = json_decode((string) $response->getBody(), true);
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));
return $response;
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function createMeeting(array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings';
return $this->makeRequest($endpoint, 'POST', $payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function updateMeeting(string $meetingId, array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings/' . $meetingId;
return $this->makeRequest($endpoint, 'PATCH', $payload);
}
/**
* @throws \Exception
*/
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string {
try {
$noteInput = new SimplePublicObjectInput([
'properties' => [
'hs_note_body' => $body,
'hubspot_owner_id' => $ownerId,
'hs_timestamp' => $timestamp,
],
]);
// Create note
$note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);
$this->getNewInstance()->crm()->objects()->associationsApi()->create(
'note',
$note->getId(),
$this->getNoteObject($noteObject),
$objectId,
$this->getNoteAssociationType($noteObject),
);
return $note->getId();
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to create note', [
'objectId' => $objectId,
'noteObject' => $noteObject->getObjectType(),
'reason' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return null;
}
public function updateEngagement(string $objectId, array $engagement, array $metadata): void
{
$this->getInstance()->engagements()->update($objectId, $engagement, $metadata);
}
public function getEngagementData(string $engagementId): array
{
$engagement = $this->getInstance()->engagements()->get($engagementId);
return $engagement->toArray();
}
public function createEngagement(array $engagement, array $associations, array $metadata): Response
{
return $this->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
}
public function isUnauthorizedException(\Exception $e): bool
{
// Check for specific HubSpot API exception types first
if ($e instanceof BadRequest) {
// BadRequest can contain 401 status codes
return $e->getCode() === 401;
}
// Check for HTTP client exceptions with status codes
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$response = $e->getResponse();
if ($response !== null) {
return $response->getStatusCode() === 401;
}
}
// Check for Guzzle HTTP exceptions
if ($e instanceof \GuzzleHttp\Exception\ClientException) {
return $e->getCode() === 401;
}
// Fallback to string matching as last resort, but be more specific
$message = strtolower($e->getMessage());
return str_contains($message, '401 unauthorized') ||
str_contains($message, 'http 401') ||
str_contains($message, 'status code 401') ||
(preg_match('/\b401\b/', $message) && str_contains($message, 'unauthorized'));
}
/**
* Validates and refreshes the access token if needed before API requests.
* This ensures long-running processes don't fail due to token expiration.
*
* @throws SocialAccountTokenInvalidException
*/
public function ensureValidToken(): void
{
if ($this->oauthAccount === null) {
return;
}
$newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);
if ($newToken !== null) {
$this->accessToken = $newToken;
}
}
public function getConfig()
{
return $this->config;
}
// returns only active (archived=false)
public function getOwners(): array
{
return $this->getNewInstance()->crm()->owners()->getAll();
}
/**
* @param bool $archived
*
* @return array<Owner>|[]
*/
public function getOwnersArchived(bool $archived = true): array
{
$endpoint = '/crm/v3/owners';
$queryParams = [
'archived' => $archived ? 'true' : 'false',
];
$queryString = http_build_query($queryParams);
$owners = [];
try {
$response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);
$responseData = $response?->toArray();
foreach ($responseData['results'] as $result) {
try {
$owners[] = Owner::create($result);
} catch (Throwable $e) {
$this->log->error('[HubSpot] Failed to process owner data', [
'result' => $result,
'error' => $e->getMessage(),
]);
continue;
}
}
} catch (Throwable $e) {
$this->log->error('HubSpot] Failed to fetch owners', [
'archived' => $archived,
'error' => $e->getMessage(),
]);
return [];
}
return $owners;
}
public function getMeeting(string $engagementId): ObjectWithAssociations
{
return $this->getNewInstance()->crm()->objects()->basicApi()
->getById('meeting', $engagementId, null, 'contact,company,deal');
}
public function deleteEngagement(string $engagementId): void
{
$this->getInstance()->engagements()->delete((int) $engagementId);
}
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array
{
$associationData = [];
$idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);
foreach ($idChunks as $idChunk) {
try {
$batchInput = new \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId();
$batchInput->setInputs(array_map(function ($id) {
$publicObjectId = new \HubSpot\Client\Crm\Associations\Model\PublicObjectId();
$publicObjectId->setId($id);
return $publicObjectId;
}, $idChunk));
$associatedObjectsData = $this
->getNewInstance()
->crm()
->associations()
->batchApi()
->read($fromObject, $toObject, $batchInput);
if ($associatedObjectsData instanceof \HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti) {
foreach ($associatedObjectsData->getResults() as $association) {
$from = $association->getFrom()->getId();
$toAssociations = $association->getTo();
if (! empty($toAssociations)) {
$associationData[$from] = array_map(function ($item) {
return $item->getId();
}, $toAssociations);
}
}
}
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to fetch associations', [
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => $e->getMessage(),
]);
}
}
return $associationData;
}
/**
* @throws \Exception
*/
private function getNoteAssociationType(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'note_to_deal',
NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it
NoteObject::Account => 'note_to_company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
/**
* @throws \Exception
*/
private function getNoteObject(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'deal',
NoteObject::Lead, NoteObject::Contact => 'contact',
NoteObject::Account => 'company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
public function addAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/create";
return $this->makeRequest($endpoint, 'POST', $payload);
}
public function removeAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/archive";
return $this->makeRequest($endpoint, 'POST', $payload);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
2867
|
NULL
|
NULL
|
NULL
|
|
2868
|
113
|
50
|
2026-05-07T11:43:49.771249+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778154229771_m1.jpg...
|
PhpStorm
|
faVsco.js – Hubspot/Client.php
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
Editor for custom.log
Sync Changes
Hide This Notification
Code changed:
Hide
2
60
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot;
use HubSpot\Client\Crm\Deals\ApiException as DealApiException;
use HubSpot\Client\Crm\Contacts\ApiException as ContactApiException;
use HubSpot\Client\Crm\Companies\ApiException as CompanyApiException;
use HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations as ContactsWithAssociations;
use HubSpot\Client\Crm\Companies\Model\SimplePublicObjectWithAssociations as CompaniesWithAssociations;
use HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations as DealWithAssociations;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectInput;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectWithAssociations as ObjectWithAssociations;
use HubSpot\Client\Crm\Pipelines\Model\Error;
use HubSpot\Client\Crm\Pipelines\Model\PipelineStage;
use HubSpot\Client\Crm\Properties\Model\Property;
use HubSpot\Discovery\Discovery;
use Jiminny\Component\Utility\Service\ProviderRateLimiter;
use Jiminny\Exceptions\CrmException;
use Jiminny\Exceptions\RateLimitException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Jobs\Crm\NoteObject;
use Jiminny\Models\Crm\Field;
use Jiminny\Services\Crm\BaseClient;
use Jiminny\Services\Crm\Hubspot\DTO\Response\Owner;
use Jiminny\Services\SocialAccountService;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use SevenShores\Hubspot\Factory;
use SevenShores\Hubspot\Http\Response;
use Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService;
use Throwable;
/**
* @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
* @param callable(): T $apiCall
* @return T
*
* @throws RateLimitException
*/
private function executeRequest(callable $apiCall)
{
if (! $this->rateLimiter->canMakeRequest($this->config)) {
$retryAfter = $this->rateLimiter->requestAvailableIn($this->config);
$this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
]);
throw new RateLimitException(
'Hubspot rate limit reached for configuration ' . $this->config->getId(),
$retryAfter,
);
}
$this->rateLimiter->incrementRequestCount($this->config);
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function isHubspotRateLimit(Throwable $e): bool
{
return method_exists($e, 'getCode') && (int) $e->getCode() === 429;
}
private function parseRetryAfter(Throwable $e): int
{
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
return 10;
}
public function getMinimumApiVersion(): string
{
return self::MIN_API_VERSION;
}
public function getInstance(): Factory
{
return new Factory([
'key' => $this->accessToken,
'oauth2' => true,
'base_url' => $this->baseUrl,
]);
}
public function getNewInstance(): Discovery
{
return \HubSpot\Factory::createWithAccessToken($this->accessToken);
}
/**
* Secondly and daily limits for Hubspot API
*
* Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)
* Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds
* Daily: 250,000 | 500,000 | 1,000,000
*
* Official documentation states: The search endpoints are rate limited to five requests per second.
* Since with 5 RPS were still hitting secondly rate limits we lowered it to 4
*/
public function getPaginatedData(array $payload, string $type, int $offset = 0): array
{
$total = 0;
$lastId = null;
$rows = [];
foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {
$rows[] = $row;
}
return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
return $this->paginationService->getPaginatedDataGenerator(
$this,
$payload,
$type,
$offset,
$total,
$lastRecordId
);
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
$deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
));
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $batchConfig['api']->read($batchReadRequest);
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
$response = $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
$response = $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
$max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // "110"
$remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // "109"
$interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // "10000"
$body = json_decode((string) $response->getBody(), true);
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));
return $response;
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function createMeeting(array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings';
return $this->makeRequest($endpoint, 'POST', $payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function updateMeeting(string $meetingId, array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings/' . $meetingId;
return $this->makeRequest($endpoint, 'PATCH', $payload);
}
/**
* @throws \Exception
*/
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string {
try {
$noteInput = new SimplePublicObjectInput([
'properties' => [
'hs_note_body' => $body,
'hubspot_owner_id' => $ownerId,
'hs_timestamp' => $timestamp,
],
]);
// Create note
$note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);
$this->getNewInstance()->crm()->objects()->associationsApi()->create(
'note',
$note->getId(),
$this->getNoteObject($noteObject),
$objectId,
$this->getNoteAssociationType($noteObject),
);
return $note->getId();
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to create note', [
'objectId' => $objectId,
'noteObject' => $noteObject->getObjectType(),
'reason' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return null;
}
public function updateEngagement(string $objectId, array $engagement, array $metadata): void
{
$this->getInstance()->engagements()->update($objectId, $engagement, $metadata);
}
public function getEngagementData(string $engagementId): array
{
$engagement = $this->getInstance()->engagements()->get($engagementId);
return $engagement->toArray();
}
public function createEngagement(array $engagement, array $associations, array $metadata): Response
{
return $this->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
}
public function isUnauthorizedException(\Exception $e): bool
{
// Check for specific HubSpot API exception types first
if ($e instanceof BadRequest) {
// BadRequest can contain 401 status codes
return $e->getCode() === 401;
}
// Check for HTTP client exceptions with status codes
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$response = $e->getResponse();
if ($response !== null) {
return $response->getStatusCode() === 401;
}
}
// Check for Guzzle HTTP exceptions
if ($e instanceof \GuzzleHttp\Exception\ClientException) {
return $e->getCode() === 401;
}
// Fallback to string matching as last resort, but be more specific
$message = strtolower($e->getMessage());
return str_contains($message, '401 unauthorized') ||
str_contains($message, 'http 401') ||
str_contains($message, 'status code 401') ||
(preg_match('/\b401\b/', $message) && str_contains($message, 'unauthorized'));
}
/**
* Validates and refreshes the access token if needed before API requests.
* This ensures long-running processes don't fail due to token expiration.
*
* @throws SocialAccountTokenInvalidException
*/
public function ensureValidToken(): void
{
if ($this->oauthAccount === null) {
return;
}
$newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);
if ($newToken !== null) {
$this->accessToken = $newToken;
}
}
public function getConfig()
{
return $this->config;
}
// returns only active (archived=false)
public function getOwners(): array
{
return $this->getNewInstance()->crm()->owners()->getAll();
}
/**
* @param bool $archived
*
* @return array<Owner>|[]
*/
public function getOwnersArchived(bool $archived = true): array
{
$endpoint = '/crm/v3/owners';
$queryParams = [
'archived' => $archived ? 'true' : 'false',
];
$queryString = http_build_query($queryParams);
$owners = [];
try {
$response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);
$responseData = $response?->toArray();
foreach ($responseData['results'] as $result) {
try {
$owners[] = Owner::create($result);
} catch (Throwable $e) {
$this->log->error('[HubSpot] Failed to process owner data', [
'result' => $result,
'error' => $e->getMessage(),
]);
continue;
}
}
} catch (Throwable $e) {
$this->log->error('HubSpot] Failed to fetch owners', [
'archived' => $archived,
'error' => $e->getMessage(),
]);
return [];
}
return $owners;
}
public function getMeeting(string $engagementId): ObjectWithAssociations
{
return $this->getNewInstance()->crm()->objects()->basicApi()
->getById('meeting', $engagementId, null, 'contact,company,deal');
}
public function deleteEngagement(string $engagementId): void
{
$this->getInstance()->engagements()->delete((int) $engagementId);
}
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array
{
$associationData = [];
$idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);
foreach ($idChunks as $idChunk) {
try {
$batchInput = new \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId();
$batchInput->setInputs(array_map(function ($id) {
$publicObjectId = new \HubSpot\Client\Crm\Associations\Model\PublicObjectId();
$publicObjectId->setId($id);
return $publicObjectId;
}, $idChunk));
$associatedObjectsData = $this
->getNewInstance()
->crm()
->associations()
->batchApi()
->read($fromObject, $toObject, $batchInput);
if ($associatedObjectsData instanceof \HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti) {
foreach ($associatedObjectsData->getResults() as $association) {
$from = $association->getFrom()->getId();
$toAssociations = $association->getTo();
if (! empty($toAssociations)) {
$associationData[$from] = array_map(function ($item) {
return $item->getId();
}, $toAssociations);
}
}
}
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to fetch associations', [
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => $e->getMessage(),
]);
}
}
return $associationData;
}
/**
* @throws \Exception
*/
private function getNoteAssociationType(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'note_to_deal',
NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it
NoteObject::Account => 'note_to_company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
/**
* @throws \Exception
*/
private function getNoteObject(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'deal',
NoteObject::Lead, NoteObject::Contact => 'contact',
NoteObject::Account => 'company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
public function addAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/create";
return $this->makeRequest($endpoint, 'POST', $payload);
}
public function removeAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/archive";
return $this->makeRequest($endpoint, 'POST', $payload);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"on_screen":true,"help_text":"Git Branch: master","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"Editor for custom.log","depth":4,"on_screen":true,"role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.016666668,"height":0.02111111},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"60","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.021527778,"height":0.02111111},"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.015277778,"height":0.025555555},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.014583333,"height":0.025555555},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Component\\Utility\\Service\\ProviderRateLimiter;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n private ProviderRateLimiter $rateLimiter;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager,\n ProviderRateLimiter $rateLimiter,\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n $this->rateLimiter = $rateLimiter;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Single entry point for every HubSpot API call. Enforces the per-portal\n * rate limit configured in the rate_limits table (morphed to the current\n * Configuration) and reacts to a real 429 from HubSpot by translating it\n * into a RateLimitException carrying Retry-After.\n *\n * Wrap any outbound HubSpot call (SDK or raw HTTP) like:\n *\n * $this->executeRequest(fn () => $this->getNewInstance()->crm()->...);\n *\n * @template T\n * @param callable(): T $apiCall\n * @return T\n *\n * @throws RateLimitException\n */\n private function executeRequest(callable $apiCall)\n {\n if (! $this->rateLimiter->canMakeRequest($this->config)) {\n $retryAfter = $this->rateLimiter->requestAvailableIn($this->config);\n\n $this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n ]);\n\n throw new RateLimitException(\n 'Hubspot rate limit reached for configuration ' . $this->config->getId(),\n $retryAfter,\n );\n }\n\n $this->rateLimiter->incrementRequestCount($this->config);\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n private function isHubspotRateLimit(Throwable $e): bool\n {\n return method_exists($e, 'getCode') && (int) $e->getCode() === 429;\n }\n\n private function parseRetryAfter(Throwable $e): int\n {\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n return 10;\n }\n\n public function getMinimumApiVersion(): string\n {\n return self::MIN_API_VERSION;\n }\n\n public function getInstance(): Factory\n {\n return new Factory([\n 'key' => $this->accessToken,\n 'oauth2' => true,\n 'base_url' => $this->baseUrl,\n ]);\n }\n\n public function getNewInstance(): Discovery\n {\n return \\HubSpot\\Factory::createWithAccessToken($this->accessToken);\n }\n\n /**\n * Secondly and daily limits for Hubspot API\n *\n * Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)\n * Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds\n * Daily: 250,000 | 500,000 | 1,000,000\n *\n * Official documentation states: The search endpoints are rate limited to five requests per second.\n * Since with 5 RPS were still hitting secondly rate limits we lowered it to 4\n */\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array\n {\n $total = 0;\n $lastId = null;\n $rows = [];\n foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {\n $rows[] = $row;\n }\n\n return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n return $this->paginationService->getPaginatedDataGenerator(\n $this,\n $payload,\n $type,\n $offset,\n $total,\n $lastRecordId\n );\n }\n\n /**\n * @throws DealApiException\n * @throws CrmException\n */\n public function getOpportunityById(string $crmId, array $fields): array\n {\n try {\n $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n ));\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n $response = $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n $response = $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n\n $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // \"110\"\n $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // \"109\"\n $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // \"10000\"\n $body = json_decode((string) $response->getBody(), true);\n\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));\n\n return $response;\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Component\\Utility\\Service\\ProviderRateLimiter;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n private ProviderRateLimiter $rateLimiter;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager,\n ProviderRateLimiter $rateLimiter,\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n $this->rateLimiter = $rateLimiter;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Single entry point for every HubSpot API call. Enforces the per-portal\n * rate limit configured in the rate_limits table (morphed to the current\n * Configuration) and reacts to a real 429 from HubSpot by translating it\n * into a RateLimitException carrying Retry-After.\n *\n * Wrap any outbound HubSpot call (SDK or raw HTTP) like:\n *\n * $this->executeRequest(fn () => $this->getNewInstance()->crm()->...);\n *\n * @template T\n * @param callable(): T $apiCall\n * @return T\n *\n * @throws RateLimitException\n */\n private function executeRequest(callable $apiCall)\n {\n if (! $this->rateLimiter->canMakeRequest($this->config)) {\n $retryAfter = $this->rateLimiter->requestAvailableIn($this->config);\n\n $this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n ]);\n\n throw new RateLimitException(\n 'Hubspot rate limit reached for configuration ' . $this->config->getId(),\n $retryAfter,\n );\n }\n\n $this->rateLimiter->incrementRequestCount($this->config);\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n private function isHubspotRateLimit(Throwable $e): bool\n {\n return method_exists($e, 'getCode') && (int) $e->getCode() === 429;\n }\n\n private function parseRetryAfter(Throwable $e): int\n {\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n return 10;\n }\n\n public function getMinimumApiVersion(): string\n {\n return self::MIN_API_VERSION;\n }\n\n public function getInstance(): Factory\n {\n return new Factory([\n 'key' => $this->accessToken,\n 'oauth2' => true,\n 'base_url' => $this->baseUrl,\n ]);\n }\n\n public function getNewInstance(): Discovery\n {\n return \\HubSpot\\Factory::createWithAccessToken($this->accessToken);\n }\n\n /**\n * Secondly and daily limits for Hubspot API\n *\n * Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)\n * Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds\n * Daily: 250,000 | 500,000 | 1,000,000\n *\n * Official documentation states: The search endpoints are rate limited to five requests per second.\n * Since with 5 RPS were still hitting secondly rate limits we lowered it to 4\n */\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array\n {\n $total = 0;\n $lastId = null;\n $rows = [];\n foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {\n $rows[] = $row;\n }\n\n return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n return $this->paginationService->getPaginatedDataGenerator(\n $this,\n $payload,\n $type,\n $offset,\n $total,\n $lastRecordId\n );\n }\n\n /**\n * @throws DealApiException\n * @throws CrmException\n */\n public function getOpportunityById(string $crmId, array $fields): array\n {\n try {\n $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n ));\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n $response = $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n $response = $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n\n $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // \"110\"\n $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // \"109\"\n $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // \"10000\"\n $body = json_decode((string) $response->getBody(), true);\n\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));\n\n return $response;\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-188086479456983350
|
6378757184196315236
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
Editor for custom.log
Sync Changes
Hide This Notification
Code changed:
Hide
2
60
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot;
use HubSpot\Client\Crm\Deals\ApiException as DealApiException;
use HubSpot\Client\Crm\Contacts\ApiException as ContactApiException;
use HubSpot\Client\Crm\Companies\ApiException as CompanyApiException;
use HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations as ContactsWithAssociations;
use HubSpot\Client\Crm\Companies\Model\SimplePublicObjectWithAssociations as CompaniesWithAssociations;
use HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations as DealWithAssociations;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectInput;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectWithAssociations as ObjectWithAssociations;
use HubSpot\Client\Crm\Pipelines\Model\Error;
use HubSpot\Client\Crm\Pipelines\Model\PipelineStage;
use HubSpot\Client\Crm\Properties\Model\Property;
use HubSpot\Discovery\Discovery;
use Jiminny\Component\Utility\Service\ProviderRateLimiter;
use Jiminny\Exceptions\CrmException;
use Jiminny\Exceptions\RateLimitException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Jobs\Crm\NoteObject;
use Jiminny\Models\Crm\Field;
use Jiminny\Services\Crm\BaseClient;
use Jiminny\Services\Crm\Hubspot\DTO\Response\Owner;
use Jiminny\Services\SocialAccountService;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use SevenShores\Hubspot\Factory;
use SevenShores\Hubspot\Http\Response;
use Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService;
use Throwable;
/**
* @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
* @param callable(): T $apiCall
* @return T
*
* @throws RateLimitException
*/
private function executeRequest(callable $apiCall)
{
if (! $this->rateLimiter->canMakeRequest($this->config)) {
$retryAfter = $this->rateLimiter->requestAvailableIn($this->config);
$this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
]);
throw new RateLimitException(
'Hubspot rate limit reached for configuration ' . $this->config->getId(),
$retryAfter,
);
}
$this->rateLimiter->incrementRequestCount($this->config);
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function isHubspotRateLimit(Throwable $e): bool
{
return method_exists($e, 'getCode') && (int) $e->getCode() === 429;
}
private function parseRetryAfter(Throwable $e): int
{
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
return 10;
}
public function getMinimumApiVersion(): string
{
return self::MIN_API_VERSION;
}
public function getInstance(): Factory
{
return new Factory([
'key' => $this->accessToken,
'oauth2' => true,
'base_url' => $this->baseUrl,
]);
}
public function getNewInstance(): Discovery
{
return \HubSpot\Factory::createWithAccessToken($this->accessToken);
}
/**
* Secondly and daily limits for Hubspot API
*
* Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)
* Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds
* Daily: 250,000 | 500,000 | 1,000,000
*
* Official documentation states: The search endpoints are rate limited to five requests per second.
* Since with 5 RPS were still hitting secondly rate limits we lowered it to 4
*/
public function getPaginatedData(array $payload, string $type, int $offset = 0): array
{
$total = 0;
$lastId = null;
$rows = [];
foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {
$rows[] = $row;
}
return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
return $this->paginationService->getPaginatedDataGenerator(
$this,
$payload,
$type,
$offset,
$total,
$lastRecordId
);
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
$deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
));
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $batchConfig['api']->read($batchReadRequest);
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
$response = $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
$response = $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
$max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // "110"
$remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // "109"
$interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // "10000"
$body = json_decode((string) $response->getBody(), true);
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));
return $response;
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function createMeeting(array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings';
return $this->makeRequest($endpoint, 'POST', $payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function updateMeeting(string $meetingId, array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings/' . $meetingId;
return $this->makeRequest($endpoint, 'PATCH', $payload);
}
/**
* @throws \Exception
*/
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string {
try {
$noteInput = new SimplePublicObjectInput([
'properties' => [
'hs_note_body' => $body,
'hubspot_owner_id' => $ownerId,
'hs_timestamp' => $timestamp,
],
]);
// Create note
$note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);
$this->getNewInstance()->crm()->objects()->associationsApi()->create(
'note',
$note->getId(),
$this->getNoteObject($noteObject),
$objectId,
$this->getNoteAssociationType($noteObject),
);
return $note->getId();
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to create note', [
'objectId' => $objectId,
'noteObject' => $noteObject->getObjectType(),
'reason' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return null;
}
public function updateEngagement(string $objectId, array $engagement, array $metadata): void
{
$this->getInstance()->engagements()->update($objectId, $engagement, $metadata);
}
public function getEngagementData(string $engagementId): array
{
$engagement = $this->getInstance()->engagements()->get($engagementId);
return $engagement->toArray();
}
public function createEngagement(array $engagement, array $associations, array $metadata): Response
{
return $this->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
}
public function isUnauthorizedException(\Exception $e): bool
{
// Check for specific HubSpot API exception types first
if ($e instanceof BadRequest) {
// BadRequest can contain 401 status codes
return $e->getCode() === 401;
}
// Check for HTTP client exceptions with status codes
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$response = $e->getResponse();
if ($response !== null) {
return $response->getStatusCode() === 401;
}
}
// Check for Guzzle HTTP exceptions
if ($e instanceof \GuzzleHttp\Exception\ClientException) {
return $e->getCode() === 401;
}
// Fallback to string matching as last resort, but be more specific
$message = strtolower($e->getMessage());
return str_contains($message, '401 unauthorized') ||
str_contains($message, 'http 401') ||
str_contains($message, 'status code 401') ||
(preg_match('/\b401\b/', $message) && str_contains($message, 'unauthorized'));
}
/**
* Validates and refreshes the access token if needed before API requests.
* This ensures long-running processes don't fail due to token expiration.
*
* @throws SocialAccountTokenInvalidException
*/
public function ensureValidToken(): void
{
if ($this->oauthAccount === null) {
return;
}
$newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);
if ($newToken !== null) {
$this->accessToken = $newToken;
}
}
public function getConfig()
{
return $this->config;
}
// returns only active (archived=false)
public function getOwners(): array
{
return $this->getNewInstance()->crm()->owners()->getAll();
}
/**
* @param bool $archived
*
* @return array<Owner>|[]
*/
public function getOwnersArchived(bool $archived = true): array
{
$endpoint = '/crm/v3/owners';
$queryParams = [
'archived' => $archived ? 'true' : 'false',
];
$queryString = http_build_query($queryParams);
$owners = [];
try {
$response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);
$responseData = $response?->toArray();
foreach ($responseData['results'] as $result) {
try {
$owners[] = Owner::create($result);
} catch (Throwable $e) {
$this->log->error('[HubSpot] Failed to process owner data', [
'result' => $result,
'error' => $e->getMessage(),
]);
continue;
}
}
} catch (Throwable $e) {
$this->log->error('HubSpot] Failed to fetch owners', [
'archived' => $archived,
'error' => $e->getMessage(),
]);
return [];
}
return $owners;
}
public function getMeeting(string $engagementId): ObjectWithAssociations
{
return $this->getNewInstance()->crm()->objects()->basicApi()
->getById('meeting', $engagementId, null, 'contact,company,deal');
}
public function deleteEngagement(string $engagementId): void
{
$this->getInstance()->engagements()->delete((int) $engagementId);
}
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array
{
$associationData = [];
$idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);
foreach ($idChunks as $idChunk) {
try {
$batchInput = new \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId();
$batchInput->setInputs(array_map(function ($id) {
$publicObjectId = new \HubSpot\Client\Crm\Associations\Model\PublicObjectId();
$publicObjectId->setId($id);
return $publicObjectId;
}, $idChunk));
$associatedObjectsData = $this
->getNewInstance()
->crm()
->associations()
->batchApi()
->read($fromObject, $toObject, $batchInput);
if ($associatedObjectsData instanceof \HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti) {
foreach ($associatedObjectsData->getResults() as $association) {
$from = $association->getFrom()->getId();
$toAssociations = $association->getTo();
if (! empty($toAssociations)) {
$associationData[$from] = array_map(function ($item) {
return $item->getId();
}, $toAssociations);
}
}
}
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to fetch associations', [
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => $e->getMessage(),
]);
}
}
return $associationData;
}
/**
* @throws \Exception
*/
private function getNoteAssociationType(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'note_to_deal',
NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it
NoteObject::Account => 'note_to_company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
/**
* @throws \Exception
*/
private function getNoteObject(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'deal',
NoteObject::Lead, NoteObject::Contact => 'contact',
NoteObject::Account => 'company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
public function addAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/create";
return $this->makeRequest($endpoint, 'POST', $payload);
}
public function removeAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/archive";
return $this->makeRequest($endpoint, 'POST', $payload);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
2867
|
114
|
49
|
2026-05-07T11:43:47.229813+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778154227229_m2.jpg...
|
PhpStorm
|
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Find in Files
97 matches in 21 files
File mask:
*. Find in Files
97 matches in 21 files
File mask:
*.php
*.php
Auto
*.php
Filter Search Results
Pin Window
Search History
makeRequest
New Line
Match case
Words
Regex
Replace History
Replace
New Line
Preserve case
In Project
Module
Directory
Scope
Module
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot
/Users/lukas/jiminny/app/app/Exceptions
/Users/lukas/jiminny/app/app/Listeners/Crm
/Users/lukas/jiminny/app/app/Component/Queue/Job
/Users/lukas/jiminny/app/app/Listeners/AutomatedReports/UserPilot
/Users/lukas/jiminny/app/app/Events/Crm
/Users/lukas/jiminny/app/app/Jobs/AutomatedReports
/Users/lukas/jiminny/app/app/Listeners/Activities/Coaching/UserPilot
/Users/lukas/jiminny/app/app/Listeners/Activities/ActivityProvider/UserPilot
/Users/lukas/jiminny/app/app/Jobs/Activity/PushSummaryToCrm
/Users/lukas/jiminny/app/app/Repositories/Crm
/Users/lukas/jiminny/app/app/Jobs/Crm/Delete
/Users/lukas/jiminny/app/app/Services/Kiosk/AutomatedReports
/Users/lukas/jiminny/app/app/Http/Controllers/API/UserAutomatedReports
/Users/lukas/jiminny/app/app/Services/Crm/Salesforce
/Users/lukas/jiminny/app/app/Providers
/Users/lukas/jiminny/app/app/Services/Crm/IntegrationApp
/Users/lukas/jiminny/app/app/Events/Activities/Crm
/Users/lukas/jiminny/app/app/Listeners/Playbooks
/Users/lukas/jiminny/app/app/Console/Commands/Crm
/Users/lukas/jiminny/app/app/Services/Crm
/Users/lukas/jiminny/app/app/Http/Controllers
/Users/lukas/jiminny/app/app/Console/Commands/Reports
/Users/lukas/jiminny/app/app/VO/Repository/OnDemandActivitySearch
/Users/lukas/jiminny/app/app/Listeners/Activities/Conferences/UserPilot
/Users/lukas/jiminny/app/app/Http/Controllers/Webhook
/Users/lukas/jiminny/app/resources/views/emails/reports
/Users/lukas/jiminny/app/app/Mail/Reports
/Users/lukas/jiminny/app/app/Repositories
/Users/lukas/jiminny/app/app/Component/ActivitySearch/Service
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Salesforce
/Users/lukas/jiminny/app/routes
/Users/lukas/jiminny/app/app/Console/Commands
/Users/lukas/jiminny/app/database/migrations
/Users/lukas/Library/Application Support/JetBrains/PhpStorm2026.1/consoles/db/325d461a-c90f-430a-99d4-6ddfce0c61d7
/Users/lukas/jiminny/app/app/Http/Controllers/API/V2
/Users/lukas/jiminny/app/app/Jobs/Crm/Hubspot
/Users/lukas/jiminny/app/app/Component/DealInsights
/Users/lukas/jiminny/app/app/Policies
/Users/lukas/jiminny/app/app/Services/Crm/Helpers
/Users/lukas/jiminny/app/app/Jobs/Crm
/Users/lukas/jiminny/app/app/Models
/Users/lukas/jiminny/app/app/Listeners/Teams
/Users/lukas/jiminny/app/app/Jobs/Crm/Salesforce
/Users/lukas/jiminny/app/app
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/Journal
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/ServiceTraits
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/OpportunitySyncStrategy
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/Webhook
/Users/lukas/jiminny/app/storage/logs
/Users/lukas/jiminny/app
/Users/lukas/jiminny/app/app/Services/Internal
/Users/lukas/jiminny/app/app/Listeners/Transcription
/Users/lukas/jiminny/app/tests/Unit/Listeners/Teams
/Users/lukas/jiminny/app/app/Models/Crm
/Users/lukas/Library/Application Support/JetBrains/PhpStorm2026.1/consoles/db/91133dfa-8d71-4e12-bfb8-fec7f1afba8f
/Users/lukas/jiminny/app/app/Observers
/Users/lukas/jiminny/app/app/Services/Mail
/Users/lukas/jiminny/app/app/Console/Commands/Activities
/Users/lukas/jiminny/app/app/Console/Commands/Activities/Migrator
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot/ServiceTraits
/Users/lukas/jiminny/app/app/Jobs/User
/Users/lukas/jiminny/app/app/Models/Activity
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot/Webhook
/Users/lukas/jiminny/app/app/Component/AiAutomation/Listeners/PendingAnalysis
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot
/Users/lukas/jiminny/app/app/Component/ActivitySearch/FilterDefinition/DealInsights
/Users/lukas/jiminny/app/app/Services/Crm/DecorateActivity
/Users/lukas/jiminny/app/app/Component/Activity/Event
/Users/lukas/jiminny/app/app/Component/Sidekick
/Users/lukas/jiminny/app/app/Listeners/Activities/Conferences
/Users/lukas/jiminny/app/app/Listeners/Activities/Bots
/Users/lukas/jiminny/app/app/Services/RecallAI/Webhooks/Handlers
/Users/lukas/jiminny/app/app/Events/Activities/Bots
/Users/lukas/jiminny/app/app/Component/MeetingBot
/Users/lukas/jiminny/app/app/Services/Activity/RingCentral
/Users/lukas/jiminny/app/app/Http/Controllers/Webhook/Hubspot
/Users/lukas/jiminny/app/app/Services/Activity/Gmail
/Users/lukas/jiminny/app/app/Services/Crm/CrmObjects/ServiceTraits
/Users/lukas/jiminny/app/app/Jobs/Mailbox
/Users/lukas/jiminny/app/app/Console
/Users/lukas/jiminny/app/front-end/src/composables
/Users/lukas/jiminny/app/app/Console/Commands/Calendars
/Users/lukas/jiminny/app/app/Http/Controllers/API
/Users/lukas/jiminny/app/app/Http/Controllers/Internal/WebhookReceiver
/Users/lukas/jiminny/app/app/Services/Crm/IntegrationApp/ServiceTraits
/Users/lukas/jiminny/app/app/Component/Queue
/Users/lukas/jiminny/app/app/Console/Commands/Crm/Hubspot
/Users/lukas/jiminny/app/app/Component/Transcription/Job
/Users/lukas/jiminny/app/tests/Unit/Services/Listeners
/Users/lukas/jiminny/app/app/Services/Crm/Listeners
/Users/lukas/jiminny/app/app/Traits
/Users/lukas/jiminny/app/tests/Unit/Jobs/Crm/Hubspot
/Users/lukas/jiminny/app/tests/Unit/Services/Crm
/Users/lukas/jiminny/app/app/Services/Activity
/Users/lukas/jiminny/app/app/Services/Calendar/Command
/Users/lukas/jiminny/app/.idea/queries
/Users/lukas/jiminny/app/vendor/hubspot/api-client/codegen/Crm
/Users/lukas/jiminny/app/vendor/hubspot
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/Fields
/Users/lukas/jiminny/app/app/Services/Crm/Copper
/Users/lukas/jiminny/app/app/Services/Crm/Bullhorn
/Users/lukas/jiminny/app/app/Notifications/Channels
/Users/lukas/jiminny/app/tests/Unit
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot/Journal
/Users/lukas/jiminny/app/app/Interactions/Settings/Teams
/Users/lukas/jiminny/app/app/Exceptions/Crm
/Users/lukas/jiminny/app/vendor/hubspot/hubspot-php/src/Endpoints
/Users/lukas/jiminny/app/config
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot/OpportunitySyncStrategy
/Users/lukas/jiminny/app/vendor/laravel/framework/src/Illuminate/Redis/Connections
/Users/lukas/jiminny/app/app/Http/Controllers/Settings/Teams
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/Webhook/Traits
/Users/lukas/jiminny/app/vendor/laravel/framework/src/Illuminate/Broadcasting
/Users/lukas/jiminny/app/app/Component/FeatureFlags
/Users/lukas/jiminny/app/app/Component/Activity
/Users/lukas/jiminny/app/app/Component/ActivitySearch
/Users/lukas/jiminny/app/tests/Unit/Events/Activities/Crm
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot/Pagination
/Users/lukas/jiminny/app/app/Console/Commands/Dev
/Users/lukas/jiminny/app/front-end
/Users/lukas/jiminny/app/app/Component/Prophet
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/IntegrationApp
/Users/lukas/jiminny/app/app/Component/AskAnything
/Users/lukas/jiminny/app/app/Component/AskJiminnyAi/OnDemandLevel/Events
/Users/lukas/jiminny/app/app/Component/AskAnything/Events
/Users/lukas/jiminny/app/app/Component/AskJiminnyAi/DealLevel/Traits
/Users/lukas/jiminny/app/app/Component/ProphetAi
/Users/lukas/Library/Application Support/JetBrains/PhpStorm2026.1/consoles/db/d1e2c340-64e9-49c6-aa9a-196201874532
/Users/lukas/jiminny/app/app/Http/Controllers/API/Page
/Users/lukas/jiminny/app/front-end/src/components/ondemand/ActivityList
/Users/lukas/Library/Application Support/JetBrains/PhpStorm2026.1/consoles/db/5b1549d5-9876-4d9e-9ce3-025f12a83283
/Users/lukas/jiminny/app/app/Contracts/Repositories
/Users/lukas/jiminny/app/app/Http/Controllers/Kiosk
/Users/lukas/jiminny/app/app/Component/AiAutomation/Actions
/Users/lukas/jiminny/app/app/Services/Activity/HubSpot
/Users/lukas/jiminny/app/app/Services/Crm/Pipedrive
/Users/lukas/jiminny/app/app/Jobs/Activity/Import
/Users/lukas/jiminny/app/app/Events/Import
/Users/lukas/jiminny/app/app/Events/Activities/Dialers
/Users/lukas/jiminny/app/tests
/Users/lukas/jiminny/app/app/Events/Activities
/Users/lukas/jiminny/app/tests/Unit/Jobs/Activity/PushSummaryToCrm
/Users/lukas/jiminny/app/app/Console/Commands/Analytics
/Users/lukas/jiminny/app/tests/Unit/Services/Kiosk/AutomatedReports
/Users/lukas/jiminny/app/app/Http/Middleware
/Users/lukas/jiminny/app/app/Http/Controllers/Auth
/Users/lukas/jiminny/app/tests/Unit/Jobs/Crm
/Users/lukas/jiminny/app/app/Services/Crm/IntegrationApp/Api
/Users/lukas/jiminny/app/app/Services/Crm/IntegrationApp/Accessors
/Users/lukas/jiminny/app/app/Services/Crm/Close
/Users/lukas/jiminny/app/app/Services
/Users/lukas/jiminny/app/app/Http/Transformers
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Pipedrive
/Users/lukas/jiminny/app/app/Listeners/Activities/Crm/Summary
/Users/lukas/jiminny/app/app/Services/Activity/AmazonConnect
/Users/lukas/jiminny/app/app/Models/Participant
/Users/lukas/jiminny/app/app/Events/Activities/Connections
/Users/lukas/jiminny/app/app/Listeners/Activities/Crm
/Users/lukas/jiminny/app/app/Services/Calendar
/Users/lukas/jiminny/app/app/Jobs/DealRisks
/Users/lukas/jiminny/app/front-end/src
/Users/lukas/jiminny/app/app/Services/Crm/IntegrationApp/Accessors/MetadataAccessors
/Users/lukas/jiminny/app/app/Component/Encoding/Job
/Users/lukas/jiminny/app/app/Services/Crm/IntegrationApp/Filters
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/IntegrationApp/ServiceTraits
/Users/lukas/jiminny/app/app/Services/Activity/BaseService/Api/Token
/Users/lukas/jiminny/app/app/Services/Crm/IntegrationApp/Jobs
/Users/lukas/Library/Application Support/JetBrains/PhpStorm2026.1/scratches
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/IntegrationApp/Accessors
/Users/lukas/jiminny/app/app/Console/Commands/Migrate
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/IntegrationApp/DTO/Factories
/Users/lukas/jiminny/app/app/Services/Crm/IntegrationApp/DTO/Utils
/Users/lukas/jiminny/app/app/Services/Crm/IntegrationApp/Config
/Users/lukas/jiminny/app/app/Services/Crm/IntegrationApp/DTO/Factories
/Users/lukas/jiminny/app/app/Services/Crm/IntegrationApp/DTO/Decorators
/Users/lukas/jiminny/app/app/Component/DealInsights/QueryBuilder/Visitor
/Users/lukas/jiminny/app/app/Contracts/Crm
/Users/lukas/jiminny/app/app/Contracts/Services/Crm
/Users/lukas/jiminny/app/app/Interactions/Settings/Onboarding
/Users/lukas/jiminny/app/vendor
/Users/lukas/jiminny/app/app/Notifications/ActivityProviders
/Users/lukas/jiminny/app/app/Listeners/Playlists/Activities
/Users/lukas/jiminny/app/app/Notifications/Playlists
/Users/lukas/jiminny/app/app/Notifications
/Users/lukas/jiminny/app/app/Listeners/Activities/Following
/Users/lukas/jiminny/app/app/Listeners/Activities/Coaching
/Users/lukas/jiminny/app/app/Component/Cache
/Users/lukas/jiminny/app/tests/Unit/Notifications/Playlists
/Users/lukas/jiminny/app/app/Events/Activities/Provider...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"Find in Files","depth":1,"bounds":{"left":0.2992021,"top":0.12609737,"width":0.024601065,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"97 matches in 21 files","depth":1,"bounds":{"left":0.32779256,"top":0.12609737,"width":0.044215426,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"File mask:","depth":1,"bounds":{"left":0.5315825,"top":0.12290503,"width":0.029587766,"height":0.019952115},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXComboBox","text":"*.php","depth":1,"bounds":{"left":0.5621675,"top":0.11971269,"width":0.027925532,"height":0.027134877},"on_screen":true,"value":"*.php","role_description":"combo box","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"*.php","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Auto","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXTextField","text":"*.php","depth":2,"bounds":{"left":0.5661569,"top":0.12609737,"width":0.011635638,"height":0.013567438},"on_screen":true,"value":"*.php","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Filter Search Results","depth":1,"bounds":{"left":0.5944149,"top":0.12290503,"width":0.00731383,"height":0.017557861},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Pin Window","depth":1,"bounds":{"left":0.6037234,"top":0.12290503,"width":0.00731383,"height":0.017557861},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Search History","depth":1,"bounds":{"left":0.2962101,"top":0.15403032,"width":0.00731383,"height":0.017557861},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"makeRequest","depth":2,"bounds":{"left":0.30718085,"top":0.15403032,"width":0.26196808,"height":0.017557861},"on_screen":true,"value":"makeRequest","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"New Line","depth":1,"bounds":{"left":0.578125,"top":0.15403032,"width":0.00731383,"height":0.017557861},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Match case","depth":1,"bounds":{"left":0.5880984,"top":0.15403032,"width":0.00731383,"height":0.017557861},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Words","depth":1,"bounds":{"left":0.59674203,"top":0.15403032,"width":0.00731383,"height":0.017557861},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Regex","depth":1,"bounds":{"left":0.60538566,"top":0.15403032,"width":0.00731383,"height":0.017557861},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Replace History","depth":1,"bounds":{"left":0.27027926,"top":1.0,"width":0.00731383,"height":0.0},"on_screen":false,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"Replace","depth":2,"on_screen":false,"role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"New Line","depth":1,"bounds":{"left":0.27027926,"top":1.0,"width":0.00731383,"height":0.0},"on_screen":false,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Preserve case","depth":1,"bounds":{"left":0.27027926,"top":1.0,"width":0.00731383,"height":0.0},"on_screen":false,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"In Project","depth":2,"bounds":{"left":0.2992021,"top":0.1867518,"width":0.022938829,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Module","depth":2,"bounds":{"left":0.32214096,"top":0.1867518,"width":0.019281914,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Directory","depth":2,"bounds":{"left":0.3414229,"top":0.1867518,"width":0.022606382,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Scope","depth":2,"bounds":{"left":0.36402926,"top":0.1867518,"width":0.017287234,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"Module","depth":1,"bounds":{"left":0.27027926,"top":1.0,"width":0.099734046,"height":0.0},"on_screen":false,"role_description":"pop up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXComboBox","text":"/Users/lukas/jiminny/app/app/Services/Crm/Hubspot","depth":1,"bounds":{"left":0.27027926,"top":1.0,"width":0.1974734,"height":0.0},"on_screen":false,"value":"/Users/lukas/jiminny/app/app/Services/Crm/Hubspot","role_description":"combo box","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/Hubspot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Exceptions","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/Queue/Job","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/AutomatedReports/UserPilot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Events/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Jobs/AutomatedReports","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/Activities/Coaching/UserPilot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/Activities/ActivityProvider/UserPilot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Jobs/Activity/PushSummaryToCrm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Repositories/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Jobs/Crm/Delete","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Kiosk/AutomatedReports","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Http/Controllers/API/UserAutomatedReports","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/Salesforce","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Providers","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/IntegrationApp","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Events/Activities/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/Playbooks","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Console/Commands/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Http/Controllers","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Console/Commands/Reports","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/VO/Repository/OnDemandActivitySearch","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/Activities/Conferences/UserPilot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Http/Controllers/Webhook","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/resources/views/emails/reports","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Mail/Reports","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Repositories","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/ActivitySearch/Service","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Salesforce","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/routes","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Console/Commands","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/database/migrations","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/Library/Application Support/JetBrains/PhpStorm2026.1/consoles/db/325d461a-c90f-430a-99d4-6ddfce0c61d7","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Http/Controllers/API/V2","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Jobs/Crm/Hubspot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/DealInsights","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Policies","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/Helpers","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Jobs/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Models","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/Teams","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Jobs/Crm/Salesforce","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/Journal","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/ServiceTraits","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/OpportunitySyncStrategy","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/Webhook","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/storage/logs","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Internal","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/Transcription","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/tests/Unit/Listeners/Teams","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Models/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/Library/Application Support/JetBrains/PhpStorm2026.1/consoles/db/91133dfa-8d71-4e12-bfb8-fec7f1afba8f","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Observers","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Mail","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Console/Commands/Activities","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Console/Commands/Activities/Migrator","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot/ServiceTraits","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Jobs/User","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Models/Activity","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot/Webhook","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/AiAutomation/Listeners/PendingAnalysis","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/ActivitySearch/FilterDefinition/DealInsights","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/DecorateActivity","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/Activity/Event","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/Sidekick","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/Activities/Conferences","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/Activities/Bots","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/RecallAI/Webhooks/Handlers","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Events/Activities/Bots","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/MeetingBot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Activity/RingCentral","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Http/Controllers/Webhook/Hubspot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Activity/Gmail","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/CrmObjects/ServiceTraits","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Jobs/Mailbox","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Console","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/front-end/src/composables","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Console/Commands/Calendars","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Http/Controllers/API","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Http/Controllers/Internal/WebhookReceiver","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/IntegrationApp/ServiceTraits","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/Queue","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Console/Commands/Crm/Hubspot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/Transcription/Job","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/tests/Unit/Services/Listeners","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/Listeners","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Traits","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/tests/Unit/Jobs/Crm/Hubspot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/tests/Unit/Services/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Activity","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Calendar/Command","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/.idea/queries","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/vendor/hubspot/api-client/codegen/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/vendor/hubspot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/Fields","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/Copper","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/Bullhorn","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Notifications/Channels","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/tests/Unit","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot/Journal","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Interactions/Settings/Teams","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Exceptions/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/vendor/hubspot/hubspot-php/src/Endpoints","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/config","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot/OpportunitySyncStrategy","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/vendor/laravel/framework/src/Illuminate/Redis/Connections","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Http/Controllers/Settings/Teams","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/Webhook/Traits","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/vendor/laravel/framework/src/Illuminate/Broadcasting","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/FeatureFlags","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/Activity","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/ActivitySearch","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/tests/Unit/Events/Activities/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot/Pagination","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Console/Commands/Dev","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/front-end","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/Prophet","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/tests/Unit/Services/Crm/IntegrationApp","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/AskAnything","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/AskJiminnyAi/OnDemandLevel/Events","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/AskAnything/Events","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/AskJiminnyAi/DealLevel/Traits","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/ProphetAi","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/Library/Application Support/JetBrains/PhpStorm2026.1/consoles/db/d1e2c340-64e9-49c6-aa9a-196201874532","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Http/Controllers/API/Page","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/front-end/src/components/ondemand/ActivityList","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/Library/Application Support/JetBrains/PhpStorm2026.1/consoles/db/5b1549d5-9876-4d9e-9ce3-025f12a83283","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Contracts/Repositories","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Http/Controllers/Kiosk","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/AiAutomation/Actions","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Activity/HubSpot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/Pipedrive","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Jobs/Activity/Import","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Events/Import","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Events/Activities/Dialers","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/tests","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Events/Activities","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/tests/Unit/Jobs/Activity/PushSummaryToCrm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Console/Commands/Analytics","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/tests/Unit/Services/Kiosk/AutomatedReports","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Http/Middleware","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Http/Controllers/Auth","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/tests/Unit/Jobs/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/IntegrationApp/Api","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/IntegrationApp/Accessors","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/Close","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Http/Transformers","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Pipedrive","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/Activities/Crm/Summary","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Activity/AmazonConnect","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Models/Participant","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Events/Activities/Connections","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/Activities/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Calendar","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Jobs/DealRisks","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/front-end/src","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/IntegrationApp/Accessors/MetadataAccessors","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/Encoding/Job","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/IntegrationApp/Filters","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/tests/Unit/Services/Crm/IntegrationApp/ServiceTraits","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Activity/BaseService/Api/Token","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/IntegrationApp/Jobs","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/Library/Application Support/JetBrains/PhpStorm2026.1/scratches","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/tests/Unit/Services/Crm/IntegrationApp/Accessors","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Console/Commands/Migrate","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/tests/Unit/Services/Crm/IntegrationApp/DTO/Factories","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/IntegrationApp/DTO/Utils","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/IntegrationApp/Config","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/IntegrationApp/DTO/Factories","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/IntegrationApp/DTO/Decorators","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/DealInsights/QueryBuilder/Visitor","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Contracts/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Contracts/Services/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Interactions/Settings/Onboarding","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/vendor","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Notifications/ActivityProviders","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/Playlists/Activities","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Notifications/Playlists","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Notifications","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/Activities/Following","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/Activities/Coaching","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/Cache","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/tests/Unit/Notifications/Playlists","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Events/Activities/Provider","depth":6,"on_screen":false,"role_description":"text"}]...
|
226240123654124956
|
-294066434803403069
|
click
|
accessibility
|
NULL
|
Find in Files
97 matches in 21 files
File mask:
*. Find in Files
97 matches in 21 files
File mask:
*.php
*.php
Auto
*.php
Filter Search Results
Pin Window
Search History
makeRequest
New Line
Match case
Words
Regex
Replace History
Replace
New Line
Preserve case
In Project
Module
Directory
Scope
Module
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot
/Users/lukas/jiminny/app/app/Exceptions
/Users/lukas/jiminny/app/app/Listeners/Crm
/Users/lukas/jiminny/app/app/Component/Queue/Job
/Users/lukas/jiminny/app/app/Listeners/AutomatedReports/UserPilot
/Users/lukas/jiminny/app/app/Events/Crm
/Users/lukas/jiminny/app/app/Jobs/AutomatedReports
/Users/lukas/jiminny/app/app/Listeners/Activities/Coaching/UserPilot
/Users/lukas/jiminny/app/app/Listeners/Activities/ActivityProvider/UserPilot
/Users/lukas/jiminny/app/app/Jobs/Activity/PushSummaryToCrm
/Users/lukas/jiminny/app/app/Repositories/Crm
/Users/lukas/jiminny/app/app/Jobs/Crm/Delete
/Users/lukas/jiminny/app/app/Services/Kiosk/AutomatedReports
/Users/lukas/jiminny/app/app/Http/Controllers/API/UserAutomatedReports
/Users/lukas/jiminny/app/app/Services/Crm/Salesforce
/Users/lukas/jiminny/app/app/Providers
/Users/lukas/jiminny/app/app/Services/Crm/IntegrationApp
/Users/lukas/jiminny/app/app/Events/Activities/Crm
/Users/lukas/jiminny/app/app/Listeners/Playbooks
/Users/lukas/jiminny/app/app/Console/Commands/Crm
/Users/lukas/jiminny/app/app/Services/Crm
/Users/lukas/jiminny/app/app/Http/Controllers
/Users/lukas/jiminny/app/app/Console/Commands/Reports
/Users/lukas/jiminny/app/app/VO/Repository/OnDemandActivitySearch
/Users/lukas/jiminny/app/app/Listeners/Activities/Conferences/UserPilot
/Users/lukas/jiminny/app/app/Http/Controllers/Webhook
/Users/lukas/jiminny/app/resources/views/emails/reports
/Users/lukas/jiminny/app/app/Mail/Reports
/Users/lukas/jiminny/app/app/Repositories
/Users/lukas/jiminny/app/app/Component/ActivitySearch/Service
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Salesforce
/Users/lukas/jiminny/app/routes
/Users/lukas/jiminny/app/app/Console/Commands
/Users/lukas/jiminny/app/database/migrations
/Users/lukas/Library/Application Support/JetBrains/PhpStorm2026.1/consoles/db/325d461a-c90f-430a-99d4-6ddfce0c61d7
/Users/lukas/jiminny/app/app/Http/Controllers/API/V2
/Users/lukas/jiminny/app/app/Jobs/Crm/Hubspot
/Users/lukas/jiminny/app/app/Component/DealInsights
/Users/lukas/jiminny/app/app/Policies
/Users/lukas/jiminny/app/app/Services/Crm/Helpers
/Users/lukas/jiminny/app/app/Jobs/Crm
/Users/lukas/jiminny/app/app/Models
/Users/lukas/jiminny/app/app/Listeners/Teams
/Users/lukas/jiminny/app/app/Jobs/Crm/Salesforce
/Users/lukas/jiminny/app/app
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/Journal
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/ServiceTraits
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/OpportunitySyncStrategy
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/Webhook
/Users/lukas/jiminny/app/storage/logs
/Users/lukas/jiminny/app
/Users/lukas/jiminny/app/app/Services/Internal
/Users/lukas/jiminny/app/app/Listeners/Transcription
/Users/lukas/jiminny/app/tests/Unit/Listeners/Teams
/Users/lukas/jiminny/app/app/Models/Crm
/Users/lukas/Library/Application Support/JetBrains/PhpStorm2026.1/consoles/db/91133dfa-8d71-4e12-bfb8-fec7f1afba8f
/Users/lukas/jiminny/app/app/Observers
/Users/lukas/jiminny/app/app/Services/Mail
/Users/lukas/jiminny/app/app/Console/Commands/Activities
/Users/lukas/jiminny/app/app/Console/Commands/Activities/Migrator
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot/ServiceTraits
/Users/lukas/jiminny/app/app/Jobs/User
/Users/lukas/jiminny/app/app/Models/Activity
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot/Webhook
/Users/lukas/jiminny/app/app/Component/AiAutomation/Listeners/PendingAnalysis
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot
/Users/lukas/jiminny/app/app/Component/ActivitySearch/FilterDefinition/DealInsights
/Users/lukas/jiminny/app/app/Services/Crm/DecorateActivity
/Users/lukas/jiminny/app/app/Component/Activity/Event
/Users/lukas/jiminny/app/app/Component/Sidekick
/Users/lukas/jiminny/app/app/Listeners/Activities/Conferences
/Users/lukas/jiminny/app/app/Listeners/Activities/Bots
/Users/lukas/jiminny/app/app/Services/RecallAI/Webhooks/Handlers
/Users/lukas/jiminny/app/app/Events/Activities/Bots
/Users/lukas/jiminny/app/app/Component/MeetingBot
/Users/lukas/jiminny/app/app/Services/Activity/RingCentral
/Users/lukas/jiminny/app/app/Http/Controllers/Webhook/Hubspot
/Users/lukas/jiminny/app/app/Services/Activity/Gmail
/Users/lukas/jiminny/app/app/Services/Crm/CrmObjects/ServiceTraits
/Users/lukas/jiminny/app/app/Jobs/Mailbox
/Users/lukas/jiminny/app/app/Console
/Users/lukas/jiminny/app/front-end/src/composables
/Users/lukas/jiminny/app/app/Console/Commands/Calendars
/Users/lukas/jiminny/app/app/Http/Controllers/API
/Users/lukas/jiminny/app/app/Http/Controllers/Internal/WebhookReceiver
/Users/lukas/jiminny/app/app/Services/Crm/IntegrationApp/ServiceTraits
/Users/lukas/jiminny/app/app/Component/Queue
/Users/lukas/jiminny/app/app/Console/Commands/Crm/Hubspot
/Users/lukas/jiminny/app/app/Component/Transcription/Job
/Users/lukas/jiminny/app/tests/Unit/Services/Listeners
/Users/lukas/jiminny/app/app/Services/Crm/Listeners
/Users/lukas/jiminny/app/app/Traits
/Users/lukas/jiminny/app/tests/Unit/Jobs/Crm/Hubspot
/Users/lukas/jiminny/app/tests/Unit/Services/Crm
/Users/lukas/jiminny/app/app/Services/Activity
/Users/lukas/jiminny/app/app/Services/Calendar/Command
/Users/lukas/jiminny/app/.idea/queries
/Users/lukas/jiminny/app/vendor/hubspot/api-client/codegen/Crm
/Users/lukas/jiminny/app/vendor/hubspot
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/Fields
/Users/lukas/jiminny/app/app/Services/Crm/Copper
/Users/lukas/jiminny/app/app/Services/Crm/Bullhorn
/Users/lukas/jiminny/app/app/Notifications/Channels
/Users/lukas/jiminny/app/tests/Unit
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot/Journal
/Users/lukas/jiminny/app/app/Interactions/Settings/Teams
/Users/lukas/jiminny/app/app/Exceptions/Crm
/Users/lukas/jiminny/app/vendor/hubspot/hubspot-php/src/Endpoints
/Users/lukas/jiminny/app/config
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot/OpportunitySyncStrategy
/Users/lukas/jiminny/app/vendor/laravel/framework/src/Illuminate/Redis/Connections
/Users/lukas/jiminny/app/app/Http/Controllers/Settings/Teams
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/Webhook/Traits
/Users/lukas/jiminny/app/vendor/laravel/framework/src/Illuminate/Broadcasting
/Users/lukas/jiminny/app/app/Component/FeatureFlags
/Users/lukas/jiminny/app/app/Component/Activity
/Users/lukas/jiminny/app/app/Component/ActivitySearch
/Users/lukas/jiminny/app/tests/Unit/Events/Activities/Crm
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot/Pagination
/Users/lukas/jiminny/app/app/Console/Commands/Dev
/Users/lukas/jiminny/app/front-end
/Users/lukas/jiminny/app/app/Component/Prophet
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/IntegrationApp
/Users/lukas/jiminny/app/app/Component/AskAnything
/Users/lukas/jiminny/app/app/Component/AskJiminnyAi/OnDemandLevel/Events
/Users/lukas/jiminny/app/app/Component/AskAnything/Events
/Users/lukas/jiminny/app/app/Component/AskJiminnyAi/DealLevel/Traits
/Users/lukas/jiminny/app/app/Component/ProphetAi
/Users/lukas/Library/Application Support/JetBrains/PhpStorm2026.1/consoles/db/d1e2c340-64e9-49c6-aa9a-196201874532
/Users/lukas/jiminny/app/app/Http/Controllers/API/Page
/Users/lukas/jiminny/app/front-end/src/components/ondemand/ActivityList
/Users/lukas/Library/Application Support/JetBrains/PhpStorm2026.1/consoles/db/5b1549d5-9876-4d9e-9ce3-025f12a83283
/Users/lukas/jiminny/app/app/Contracts/Repositories
/Users/lukas/jiminny/app/app/Http/Controllers/Kiosk
/Users/lukas/jiminny/app/app/Component/AiAutomation/Actions
/Users/lukas/jiminny/app/app/Services/Activity/HubSpot
/Users/lukas/jiminny/app/app/Services/Crm/Pipedrive
/Users/lukas/jiminny/app/app/Jobs/Activity/Import
/Users/lukas/jiminny/app/app/Events/Import
/Users/lukas/jiminny/app/app/Events/Activities/Dialers
/Users/lukas/jiminny/app/tests
/Users/lukas/jiminny/app/app/Events/Activities
/Users/lukas/jiminny/app/tests/Unit/Jobs/Activity/PushSummaryToCrm
/Users/lukas/jiminny/app/app/Console/Commands/Analytics
/Users/lukas/jiminny/app/tests/Unit/Services/Kiosk/AutomatedReports
/Users/lukas/jiminny/app/app/Http/Middleware
/Users/lukas/jiminny/app/app/Http/Controllers/Auth
/Users/lukas/jiminny/app/tests/Unit/Jobs/Crm
/Users/lukas/jiminny/app/app/Services/Crm/IntegrationApp/Api
/Users/lukas/jiminny/app/app/Services/Crm/IntegrationApp/Accessors
/Users/lukas/jiminny/app/app/Services/Crm/Close
/Users/lukas/jiminny/app/app/Services
/Users/lukas/jiminny/app/app/Http/Transformers
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Pipedrive
/Users/lukas/jiminny/app/app/Listeners/Activities/Crm/Summary
/Users/lukas/jiminny/app/app/Services/Activity/AmazonConnect
/Users/lukas/jiminny/app/app/Models/Participant
/Users/lukas/jiminny/app/app/Events/Activities/Connections
/Users/lukas/jiminny/app/app/Listeners/Activities/Crm
/Users/lukas/jiminny/app/app/Services/Calendar
/Users/lukas/jiminny/app/app/Jobs/DealRisks
/Users/lukas/jiminny/app/front-end/src
/Users/lukas/jiminny/app/app/Services/Crm/IntegrationApp/Accessors/MetadataAccessors
/Users/lukas/jiminny/app/app/Component/Encoding/Job
/Users/lukas/jiminny/app/app/Services/Crm/IntegrationApp/Filters
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/IntegrationApp/ServiceTraits
/Users/lukas/jiminny/app/app/Services/Activity/BaseService/Api/Token
/Users/lukas/jiminny/app/app/Services/Crm/IntegrationApp/Jobs
/Users/lukas/Library/Application Support/JetBrains/PhpStorm2026.1/scratches
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/IntegrationApp/Accessors
/Users/lukas/jiminny/app/app/Console/Commands/Migrate
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/IntegrationApp/DTO/Factories
/Users/lukas/jiminny/app/app/Services/Crm/IntegrationApp/DTO/Utils
/Users/lukas/jiminny/app/app/Services/Crm/IntegrationApp/Config
/Users/lukas/jiminny/app/app/Services/Crm/IntegrationApp/DTO/Factories
/Users/lukas/jiminny/app/app/Services/Crm/IntegrationApp/DTO/Decorators
/Users/lukas/jiminny/app/app/Component/DealInsights/QueryBuilder/Visitor
/Users/lukas/jiminny/app/app/Contracts/Crm
/Users/lukas/jiminny/app/app/Contracts/Services/Crm
/Users/lukas/jiminny/app/app/Interactions/Settings/Onboarding
/Users/lukas/jiminny/app/vendor
/Users/lukas/jiminny/app/app/Notifications/ActivityProviders
/Users/lukas/jiminny/app/app/Listeners/Playlists/Activities
/Users/lukas/jiminny/app/app/Notifications/Playlists
/Users/lukas/jiminny/app/app/Notifications
/Users/lukas/jiminny/app/app/Listeners/Activities/Following
/Users/lukas/jiminny/app/app/Listeners/Activities/Coaching
/Users/lukas/jiminny/app/app/Component/Cache
/Users/lukas/jiminny/app/tests/Unit/Notifications/Playlists
/Users/lukas/jiminny/app/app/Events/Activities/Provider...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
2866
|
113
|
49
|
2026-05-07T11:43:47.135322+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778154227135_m1.jpg...
|
PhpStorm
|
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Find in Files
97 matches in 21 files
File mask:
*. Find in Files
97 matches in 21 files
File mask:
*.php
*.php
Auto
*.php
Filter Search Results
Pin Window
Search History
makeRequest
New Line
Match case
Words
Regex
Replace History
Replace
New Line
Preserve case
In Project
Module
Directory
Scope
Module
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot
/Users/lukas/jiminny/app/app/Exceptions
/Users/lukas/jiminny/app/app/Listeners/Crm
/Users/lukas/jiminny/app/app/Component/Queue/Job
/Users/lukas/jiminny/app/app/Listeners/AutomatedReports/UserPilot
/Users/lukas/jiminny/app/app/Events/Crm
/Users/lukas/jiminny/app/app/Jobs/AutomatedReports
/Users/lukas/jiminny/app/app/Listeners/Activities/Coaching/UserPilot
/Users/lukas/jiminny/app/app/Listeners/Activities/ActivityProvider/UserPilot
/Users/lukas/jiminny/app/app/Jobs/Activity/PushSummaryToCrm
/Users/lukas/jiminny/app/app/Repositories/Crm
/Users/lukas/jiminny/app/app/Jobs/Crm/Delete
/Users/lukas/jiminny/app/app/Services/Kiosk/AutomatedReports
/Users/lukas/jiminny/app/app/Http/Controllers/API/UserAutomatedReports
/Users/lukas/jiminny/app/app/Services/Crm/Salesforce
/Users/lukas/jiminny/app/app/Providers
/Users/lukas/jiminny/app/app/Services/Crm/IntegrationApp
/Users/lukas/jiminny/app/app/Events/Activities/Crm
/Users/lukas/jiminny/app/app/Listeners/Playbooks
/Users/lukas/jiminny/app/app/Console/Commands/Crm
/Users/lukas/jiminny/app/app/Services/Crm
/Users/lukas/jiminny/app/app/Http/Controllers
/Users/lukas/jiminny/app/app/Console/Commands/Reports
/Users/lukas/jiminny/app/app/VO/Repository/OnDemandActivitySearch
/Users/lukas/jiminny/app/app/Listeners/Activities/Conferences/UserPilot
/Users/lukas/jiminny/app/app/Http/Controllers/Webhook
/Users/lukas/jiminny/app/resources/views/emails/reports
/Users/lukas/jiminny/app/app/Mail/Reports
/Users/lukas/jiminny/app/app/Repositories
/Users/lukas/jiminny/app/app/Component/ActivitySearch/Service
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Salesforce
/Users/lukas/jiminny/app/routes
/Users/lukas/jiminny/app/app/Console/Commands
/Users/lukas/jiminny/app/database/migrations
/Users/lukas/Library/Application Support/JetBrains/PhpStorm2026.1/consoles/db/325d461a-c90f-430a-99d4-6ddfce0c61d7
/Users/lukas/jiminny/app/app/Http/Controllers/API/V2
/Users/lukas/jiminny/app/app/Jobs/Crm/Hubspot
/Users/lukas/jiminny/app/app/Component/DealInsights
/Users/lukas/jiminny/app/app/Policies
/Users/lukas/jiminny/app/app/Services/Crm/Helpers
/Users/lukas/jiminny/app/app/Jobs/Crm
/Users/lukas/jiminny/app/app/Models
/Users/lukas/jiminny/app/app/Listeners/Teams
/Users/lukas/jiminny/app/app/Jobs/Crm/Salesforce
/Users/lukas/jiminny/app/app
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/Journal
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/ServiceTraits
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/OpportunitySyncStrategy
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/Webhook
/Users/lukas/jiminny/app/storage/logs
/Users/lukas/jiminny/app
/Users/lukas/jiminny/app/app/Services/Internal
/Users/lukas/jiminny/app/app/Listeners/Transcription
/Users/lukas/jiminny/app/tests/Unit/Listeners/Teams
/Users/lukas/jiminny/app/app/Models/Crm
/Users/lukas/Library/Application Support/JetBrains/PhpStorm2026.1/consoles/db/91133dfa-8d71-4e12-bfb8-fec7f1afba8f
/Users/lukas/jiminny/app/app/Observers
/Users/lukas/jiminny/app/app/Services/Mail
/Users/lukas/jiminny/app/app/Console/Commands/Activities
/Users/lukas/jiminny/app/app/Console/Commands/Activities/Migrator
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot/ServiceTraits
/Users/lukas/jiminny/app/app/Jobs/User
/Users/lukas/jiminny/app/app/Models/Activity
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot/Webhook
/Users/lukas/jiminny/app/app/Component/AiAutomation/Listeners/PendingAnalysis
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot
/Users/lukas/jiminny/app/app/Component/ActivitySearch/FilterDefinition/DealInsights
/Users/lukas/jiminny/app/app/Services/Crm/DecorateActivity
/Users/lukas/jiminny/app/app/Component/Activity/Event
/Users/lukas/jiminny/app/app/Component/Sidekick
/Users/lukas/jiminny/app/app/Listeners/Activities/Conferences
/Users/lukas/jiminny/app/app/Listeners/Activities/Bots
/Users/lukas/jiminny/app/app/Services/RecallAI/Webhooks/Handlers
/Users/lukas/jiminny/app/app/Events/Activities/Bots
/Users/lukas/jiminny/app/app/Component/MeetingBot
/Users/lukas/jiminny/app/app/Services/Activity/RingCentral
/Users/lukas/jiminny/app/app/Http/Controllers/Webhook/Hubspot
/Users/lukas/jiminny/app/app/Services/Activity/Gmail
/Users/lukas/jiminny/app/app/Services/Crm/CrmObjects/ServiceTraits
/Users/lukas/jiminny/app/app/Jobs/Mailbox
/Users/lukas/jiminny/app/app/Console
/Users/lukas/jiminny/app/front-end/src/composables
/Users/lukas/jiminny/app/app/Console/Commands/Calendars...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"Find in Files","depth":1,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"97 matches in 21 files","depth":1,"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"File mask:","depth":1,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXComboBox","text":"*.php","depth":1,"on_screen":true,"value":"*.php","role_description":"combo box","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"*.php","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Auto","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXTextField","text":"*.php","depth":2,"on_screen":true,"value":"*.php","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Filter Search Results","depth":1,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Pin Window","depth":1,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Search History","depth":1,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"makeRequest","depth":2,"on_screen":true,"value":"makeRequest","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"New Line","depth":1,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Match case","depth":1,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Words","depth":1,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Regex","depth":1,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Replace History","depth":1,"bounds":{"left":0.0,"top":0.0,"width":0.015277778,"height":0.024444444},"on_screen":false,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"Replace","depth":2,"on_screen":false,"role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"New Line","depth":1,"bounds":{"left":0.0,"top":0.0,"width":0.015277778,"height":0.024444444},"on_screen":false,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Preserve case","depth":1,"bounds":{"left":0.0,"top":0.0,"width":0.015277778,"height":0.024444444},"on_screen":false,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"In Project","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Module","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Directory","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Scope","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"Module","depth":1,"bounds":{"left":0.0,"top":0.0,"width":0.20833333,"height":0.037777778},"on_screen":false,"role_description":"pop up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXComboBox","text":"/Users/lukas/jiminny/app/app/Services/Crm/Hubspot","depth":1,"bounds":{"left":0.0,"top":0.0,"width":0.4125,"height":0.037777778},"on_screen":false,"value":"/Users/lukas/jiminny/app/app/Services/Crm/Hubspot","role_description":"combo box","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/Hubspot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Exceptions","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/Queue/Job","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/AutomatedReports/UserPilot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Events/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Jobs/AutomatedReports","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/Activities/Coaching/UserPilot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/Activities/ActivityProvider/UserPilot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Jobs/Activity/PushSummaryToCrm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Repositories/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Jobs/Crm/Delete","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Kiosk/AutomatedReports","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Http/Controllers/API/UserAutomatedReports","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/Salesforce","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Providers","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/IntegrationApp","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Events/Activities/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/Playbooks","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Console/Commands/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Http/Controllers","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Console/Commands/Reports","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/VO/Repository/OnDemandActivitySearch","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/Activities/Conferences/UserPilot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Http/Controllers/Webhook","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/resources/views/emails/reports","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Mail/Reports","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Repositories","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/ActivitySearch/Service","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Salesforce","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/routes","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Console/Commands","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/database/migrations","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/Library/Application Support/JetBrains/PhpStorm2026.1/consoles/db/325d461a-c90f-430a-99d4-6ddfce0c61d7","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Http/Controllers/API/V2","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Jobs/Crm/Hubspot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/DealInsights","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Policies","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/Helpers","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Jobs/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Models","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/Teams","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Jobs/Crm/Salesforce","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/Journal","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/ServiceTraits","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/OpportunitySyncStrategy","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/Webhook","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/storage/logs","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Internal","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/Transcription","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/tests/Unit/Listeners/Teams","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Models/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/Library/Application Support/JetBrains/PhpStorm2026.1/consoles/db/91133dfa-8d71-4e12-bfb8-fec7f1afba8f","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Observers","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Mail","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Console/Commands/Activities","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Console/Commands/Activities/Migrator","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot/ServiceTraits","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Jobs/User","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Models/Activity","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot/Webhook","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/AiAutomation/Listeners/PendingAnalysis","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/ActivitySearch/FilterDefinition/DealInsights","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/DecorateActivity","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/Activity/Event","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/Sidekick","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/Activities/Conferences","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/Activities/Bots","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/RecallAI/Webhooks/Handlers","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Events/Activities/Bots","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/MeetingBot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Activity/RingCentral","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Http/Controllers/Webhook/Hubspot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Activity/Gmail","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/CrmObjects/ServiceTraits","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Jobs/Mailbox","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Console","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/front-end/src/composables","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Console/Commands/Calendars","depth":6,"on_screen":false,"role_description":"text"}]...
|
-366048596337455154
|
-2746689551745595709
|
click
|
accessibility
|
NULL
|
Find in Files
97 matches in 21 files
File mask:
*. Find in Files
97 matches in 21 files
File mask:
*.php
*.php
Auto
*.php
Filter Search Results
Pin Window
Search History
makeRequest
New Line
Match case
Words
Regex
Replace History
Replace
New Line
Preserve case
In Project
Module
Directory
Scope
Module
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot
/Users/lukas/jiminny/app/app/Exceptions
/Users/lukas/jiminny/app/app/Listeners/Crm
/Users/lukas/jiminny/app/app/Component/Queue/Job
/Users/lukas/jiminny/app/app/Listeners/AutomatedReports/UserPilot
/Users/lukas/jiminny/app/app/Events/Crm
/Users/lukas/jiminny/app/app/Jobs/AutomatedReports
/Users/lukas/jiminny/app/app/Listeners/Activities/Coaching/UserPilot
/Users/lukas/jiminny/app/app/Listeners/Activities/ActivityProvider/UserPilot
/Users/lukas/jiminny/app/app/Jobs/Activity/PushSummaryToCrm
/Users/lukas/jiminny/app/app/Repositories/Crm
/Users/lukas/jiminny/app/app/Jobs/Crm/Delete
/Users/lukas/jiminny/app/app/Services/Kiosk/AutomatedReports
/Users/lukas/jiminny/app/app/Http/Controllers/API/UserAutomatedReports
/Users/lukas/jiminny/app/app/Services/Crm/Salesforce
/Users/lukas/jiminny/app/app/Providers
/Users/lukas/jiminny/app/app/Services/Crm/IntegrationApp
/Users/lukas/jiminny/app/app/Events/Activities/Crm
/Users/lukas/jiminny/app/app/Listeners/Playbooks
/Users/lukas/jiminny/app/app/Console/Commands/Crm
/Users/lukas/jiminny/app/app/Services/Crm
/Users/lukas/jiminny/app/app/Http/Controllers
/Users/lukas/jiminny/app/app/Console/Commands/Reports
/Users/lukas/jiminny/app/app/VO/Repository/OnDemandActivitySearch
/Users/lukas/jiminny/app/app/Listeners/Activities/Conferences/UserPilot
/Users/lukas/jiminny/app/app/Http/Controllers/Webhook
/Users/lukas/jiminny/app/resources/views/emails/reports
/Users/lukas/jiminny/app/app/Mail/Reports
/Users/lukas/jiminny/app/app/Repositories
/Users/lukas/jiminny/app/app/Component/ActivitySearch/Service
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Salesforce
/Users/lukas/jiminny/app/routes
/Users/lukas/jiminny/app/app/Console/Commands
/Users/lukas/jiminny/app/database/migrations
/Users/lukas/Library/Application Support/JetBrains/PhpStorm2026.1/consoles/db/325d461a-c90f-430a-99d4-6ddfce0c61d7
/Users/lukas/jiminny/app/app/Http/Controllers/API/V2
/Users/lukas/jiminny/app/app/Jobs/Crm/Hubspot
/Users/lukas/jiminny/app/app/Component/DealInsights
/Users/lukas/jiminny/app/app/Policies
/Users/lukas/jiminny/app/app/Services/Crm/Helpers
/Users/lukas/jiminny/app/app/Jobs/Crm
/Users/lukas/jiminny/app/app/Models
/Users/lukas/jiminny/app/app/Listeners/Teams
/Users/lukas/jiminny/app/app/Jobs/Crm/Salesforce
/Users/lukas/jiminny/app/app
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/Journal
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/ServiceTraits
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/OpportunitySyncStrategy
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/Webhook
/Users/lukas/jiminny/app/storage/logs
/Users/lukas/jiminny/app
/Users/lukas/jiminny/app/app/Services/Internal
/Users/lukas/jiminny/app/app/Listeners/Transcription
/Users/lukas/jiminny/app/tests/Unit/Listeners/Teams
/Users/lukas/jiminny/app/app/Models/Crm
/Users/lukas/Library/Application Support/JetBrains/PhpStorm2026.1/consoles/db/91133dfa-8d71-4e12-bfb8-fec7f1afba8f
/Users/lukas/jiminny/app/app/Observers
/Users/lukas/jiminny/app/app/Services/Mail
/Users/lukas/jiminny/app/app/Console/Commands/Activities
/Users/lukas/jiminny/app/app/Console/Commands/Activities/Migrator
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot/ServiceTraits
/Users/lukas/jiminny/app/app/Jobs/User
/Users/lukas/jiminny/app/app/Models/Activity
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot/Webhook
/Users/lukas/jiminny/app/app/Component/AiAutomation/Listeners/PendingAnalysis
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot
/Users/lukas/jiminny/app/app/Component/ActivitySearch/FilterDefinition/DealInsights
/Users/lukas/jiminny/app/app/Services/Crm/DecorateActivity
/Users/lukas/jiminny/app/app/Component/Activity/Event
/Users/lukas/jiminny/app/app/Component/Sidekick
/Users/lukas/jiminny/app/app/Listeners/Activities/Conferences
/Users/lukas/jiminny/app/app/Listeners/Activities/Bots
/Users/lukas/jiminny/app/app/Services/RecallAI/Webhooks/Handlers
/Users/lukas/jiminny/app/app/Events/Activities/Bots
/Users/lukas/jiminny/app/app/Component/MeetingBot
/Users/lukas/jiminny/app/app/Services/Activity/RingCentral
/Users/lukas/jiminny/app/app/Http/Controllers/Webhook/Hubspot
/Users/lukas/jiminny/app/app/Services/Activity/Gmail
/Users/lukas/jiminny/app/app/Services/Crm/CrmObjects/ServiceTraits
/Users/lukas/jiminny/app/app/Jobs/Mailbox
/Users/lukas/jiminny/app/app/Console
/Users/lukas/jiminny/app/front-end/src/composables
/Users/lukas/jiminny/app/app/Console/Commands/Calendars...
|
2864
|
NULL
|
NULL
|
NULL
|
|
2865
|
114
|
48
|
2026-05-07T11:43:44.741088+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778154224741_m2.jpg...
|
PhpStorm
|
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Find in Files
97 matches in 21 files
File mask:
*. Find in Files
97 matches in 21 files
File mask:
*.php
*.php
Auto
*.php
Filter Search Results
Pin Window
Search History
makeRequest
New Line
Match case
Words
Regex
Replace History
Replace
New Line
Preserve case
In Project
Module
Directory
Scope
Module
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot
/Users/lukas/jiminny/app/app/Exceptions
/Users/lukas/jiminny/app/app/Listeners/Crm
/Users/lukas/jiminny/app/app/Component/Queue/Job
/Users/lukas/jiminny/app/app/Listeners/AutomatedReports/UserPilot
/Users/lukas/jiminny/app/app/Events/Crm
/Users/lukas/jiminny/app/app/Jobs/AutomatedReports
/Users/lukas/jiminny/app/app/Listeners/Activities/Coaching/UserPilot
/Users/lukas/jiminny/app/app/Listeners/Activities/ActivityProvider/UserPilot
/Users/lukas/jiminny/app/app/Jobs/Activity/PushSummaryToCrm
/Users/lukas/jiminny/app/app/Repositories/Crm
/Users/lukas/jiminny/app/app/Jobs/Crm/Delete
/Users/lukas/jiminny/app/app/Services/Kiosk/AutomatedReports
/Users/lukas/jiminny/app/app/Http/Controllers/API/UserAutomatedReports
/Users/lukas/jiminny/app/app/Services/Crm/Salesforce
/Users/lukas/jiminny/app/app/Providers
/Users/lukas/jiminny/app/app/Services/Crm/IntegrationApp
/Users/lukas/jiminny/app/app/Events/Activities/Crm
/Users/lukas/jiminny/app/app/Listeners/Playbooks
/Users/lukas/jiminny/app/app/Console/Commands/Crm
/Users/lukas/jiminny/app/app/Services/Crm
/Users/lukas/jiminny/app/app/Http/Controllers
/Users/lukas/jiminny/app/app/Console/Commands/Reports
/Users/lukas/jiminny/app/app/VO/Repository/OnDemandActivitySearch
/Users/lukas/jiminny/app/app/Listeners/Activities/Conferences/UserPilot
/Users/lukas/jiminny/app/app/Http/Controllers/Webhook
/Users/lukas/jiminny/app/resources/views/emails/reports
/Users/lukas/jiminny/app/app/Mail/Reports
/Users/lukas/jiminny/app/app/Repositories
/Users/lukas/jiminny/app/app/Component/ActivitySearch/Service
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Salesforce
/Users/lukas/jiminny/app/routes
/Users/lukas/jiminny/app/app/Console/Commands
/Users/lukas/jiminny/app/database/migrations
/Users/lukas/Library/Application Support/JetBrains/PhpStorm2026.1/consoles/db/325d461a-c90f-430a-99d4-6ddfce0c61d7
/Users/lukas/jiminny/app/app/Http/Controllers/API/V2
/Users/lukas/jiminny/app/app/Jobs/Crm/Hubspot
/Users/lukas/jiminny/app/app/Component/DealInsights
/Users/lukas/jiminny/app/app/Policies
/Users/lukas/jiminny/app/app/Services/Crm/Helpers
/Users/lukas/jiminny/app/app/Jobs/Crm
/Users/lukas/jiminny/app/app/Models
/Users/lukas/jiminny/app/app/Listeners/Teams
/Users/lukas/jiminny/app/app/Jobs/Crm/Salesforce
/Users/lukas/jiminny/app/app
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/Journal
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/ServiceTraits
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/OpportunitySyncStrategy
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/Webhook
/Users/lukas/jiminny/app/storage/logs
/Users/lukas/jiminny/app
/Users/lukas/jiminny/app/app/Services/Internal
/Users/lukas/jiminny/app/app/Listeners/Transcription
/Users/lukas/jiminny/app/tests/Unit/Listeners/Teams
/Users/lukas/jiminny/app/app/Models/Crm
/Users/lukas/Library/Application Support/JetBrains/PhpStorm2026.1/consoles/db/91133dfa-8d71-4e12-bfb8-fec7f1afba8f
/Users/lukas/jiminny/app/app/Observers
/Users/lukas/jiminny/app/app/Services/Mail
/Users/lukas/jiminny/app/app/Console/Commands/Activities
/Users/lukas/jiminny/app/app/Console/Commands/Activities/Migrator
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot/ServiceTraits
/Users/lukas/jiminny/app/app/Jobs/User
/Users/lukas/jiminny/app/app/Models/Activity
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot/Webhook
/Users/lukas/jiminny/app/app/Component/AiAutomation/Listeners/PendingAnalysis
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot
/Users/lukas/jiminny/app/app/Component/ActivitySearch/FilterDefinition/DealInsights
/Users/lukas/jiminny/app/app/Services/Crm/DecorateActivity
/Users/lukas/jiminny/app/app/Component/Activity/Event
/Users/lukas/jiminny/app/app/Component/Sidekick
/Users/lukas/jiminny/app/app/Listeners/Activities/Conferences
/Users/lukas/jiminny/app/app/Listeners/Activities/Bots
/Users/lukas/jiminny/app/app/Services/RecallAI/Webhooks/Handlers
/Users/lukas/jiminny/app/app/Events/Activities/Bots
/Users/lukas/jiminny/app/app/Component/MeetingBot
/Users/lukas/jiminny/app/app/Services/Activity/RingCentral
/Users/lukas/jiminny/app/app/Http/Controllers/Webhook/Hubspot
/Users/lukas/jiminny/app/app/Services/Activity/Gmail
/Users/lukas/jiminny/app/app/Services/Crm/CrmObjects/ServiceTraits
/Users/lukas/jiminny/app/app/Jobs/Mailbox
/Users/lukas/jiminny/app/app/Console
/Users/lukas/jiminny/app/front-end/src/composables
/Users/lukas/jiminny/app/app/Console/Commands/Calendars
/Users/lukas/jiminny/app/app/Http/Controllers/API
/Users/lukas/jiminny/app/app/Http/Controllers/Internal/WebhookReceiver
/Users/lukas/jiminny/app/app/Services/Crm/IntegrationApp/ServiceTraits
/Users/lukas/jiminny/app/app/Component/Queue
/Users/lukas/jiminny/app/app/Console/Commands/Crm/Hubspot
/Users/lukas/jiminny/app/app/Component/Transcription/Job
/Users/lukas/jiminny/app/tests/Unit/Services/Listeners
/Users/lukas/jiminny/app/app/Services/Crm/Listeners
/Users/lukas/jiminny/app/app/Traits...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"Find in Files","depth":1,"bounds":{"left":0.2992021,"top":0.12609737,"width":0.024601065,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"97 matches in 21 files","depth":1,"bounds":{"left":0.32779256,"top":0.12609737,"width":0.044215426,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"File mask:","depth":1,"bounds":{"left":0.5315825,"top":0.12290503,"width":0.029587766,"height":0.019952115},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXComboBox","text":"*.php","depth":1,"bounds":{"left":0.5621675,"top":0.11971269,"width":0.027925532,"height":0.027134877},"on_screen":true,"value":"*.php","role_description":"combo box","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"*.php","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Auto","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXTextField","text":"*.php","depth":2,"bounds":{"left":0.5661569,"top":0.12609737,"width":0.011635638,"height":0.013567438},"on_screen":true,"value":"*.php","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Filter Search Results","depth":1,"bounds":{"left":0.5944149,"top":0.12290503,"width":0.00731383,"height":0.017557861},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Pin Window","depth":1,"bounds":{"left":0.6037234,"top":0.12290503,"width":0.00731383,"height":0.017557861},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Search History","depth":1,"bounds":{"left":0.2962101,"top":0.15403032,"width":0.00731383,"height":0.017557861},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"makeRequest","depth":2,"bounds":{"left":0.30718085,"top":0.15403032,"width":0.26196808,"height":0.017557861},"on_screen":true,"value":"makeRequest","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"New Line","depth":1,"bounds":{"left":0.578125,"top":0.15403032,"width":0.00731383,"height":0.017557861},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Match case","depth":1,"bounds":{"left":0.5880984,"top":0.15403032,"width":0.00731383,"height":0.017557861},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Words","depth":1,"bounds":{"left":0.59674203,"top":0.15403032,"width":0.00731383,"height":0.017557861},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Regex","depth":1,"bounds":{"left":0.60538566,"top":0.15403032,"width":0.00731383,"height":0.017557861},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Replace History","depth":1,"bounds":{"left":0.27027926,"top":1.0,"width":0.00731383,"height":0.0},"on_screen":false,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"Replace","depth":2,"on_screen":false,"role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"New Line","depth":1,"bounds":{"left":0.27027926,"top":1.0,"width":0.00731383,"height":0.0},"on_screen":false,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Preserve case","depth":1,"bounds":{"left":0.27027926,"top":1.0,"width":0.00731383,"height":0.0},"on_screen":false,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"In Project","depth":2,"bounds":{"left":0.2992021,"top":0.1867518,"width":0.022938829,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Module","depth":2,"bounds":{"left":0.32214096,"top":0.1867518,"width":0.019281914,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Directory","depth":2,"bounds":{"left":0.3414229,"top":0.1867518,"width":0.022606382,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Scope","depth":2,"bounds":{"left":0.36402926,"top":0.1867518,"width":0.017287234,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"Module","depth":1,"bounds":{"left":0.27027926,"top":1.0,"width":0.099734046,"height":0.0},"on_screen":false,"role_description":"pop up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXComboBox","text":"/Users/lukas/jiminny/app/app/Services/Crm/Hubspot","depth":1,"bounds":{"left":0.27027926,"top":1.0,"width":0.1974734,"height":0.0},"on_screen":false,"value":"/Users/lukas/jiminny/app/app/Services/Crm/Hubspot","role_description":"combo box","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/Hubspot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Exceptions","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/Queue/Job","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/AutomatedReports/UserPilot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Events/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Jobs/AutomatedReports","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/Activities/Coaching/UserPilot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/Activities/ActivityProvider/UserPilot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Jobs/Activity/PushSummaryToCrm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Repositories/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Jobs/Crm/Delete","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Kiosk/AutomatedReports","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Http/Controllers/API/UserAutomatedReports","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/Salesforce","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Providers","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/IntegrationApp","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Events/Activities/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/Playbooks","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Console/Commands/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Http/Controllers","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Console/Commands/Reports","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/VO/Repository/OnDemandActivitySearch","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/Activities/Conferences/UserPilot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Http/Controllers/Webhook","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/resources/views/emails/reports","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Mail/Reports","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Repositories","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/ActivitySearch/Service","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Salesforce","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/routes","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Console/Commands","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/database/migrations","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/Library/Application Support/JetBrains/PhpStorm2026.1/consoles/db/325d461a-c90f-430a-99d4-6ddfce0c61d7","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Http/Controllers/API/V2","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Jobs/Crm/Hubspot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/DealInsights","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Policies","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/Helpers","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Jobs/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Models","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/Teams","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Jobs/Crm/Salesforce","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/Journal","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/ServiceTraits","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/OpportunitySyncStrategy","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/Webhook","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/storage/logs","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Internal","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/Transcription","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/tests/Unit/Listeners/Teams","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Models/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/Library/Application Support/JetBrains/PhpStorm2026.1/consoles/db/91133dfa-8d71-4e12-bfb8-fec7f1afba8f","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Observers","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Mail","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Console/Commands/Activities","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Console/Commands/Activities/Migrator","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot/ServiceTraits","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Jobs/User","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Models/Activity","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot/Webhook","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/AiAutomation/Listeners/PendingAnalysis","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/ActivitySearch/FilterDefinition/DealInsights","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/DecorateActivity","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/Activity/Event","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/Sidekick","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/Activities/Conferences","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/Activities/Bots","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/RecallAI/Webhooks/Handlers","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Events/Activities/Bots","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/MeetingBot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Activity/RingCentral","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Http/Controllers/Webhook/Hubspot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Activity/Gmail","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/CrmObjects/ServiceTraits","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Jobs/Mailbox","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Console","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/front-end/src/composables","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Console/Commands/Calendars","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Http/Controllers/API","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Http/Controllers/Internal/WebhookReceiver","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/IntegrationApp/ServiceTraits","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/Queue","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Console/Commands/Crm/Hubspot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/Transcription/Job","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/tests/Unit/Services/Listeners","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/Listeners","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Traits","depth":6,"on_screen":false,"role_description":"text"}]...
|
-3214874653318401311
|
-296872096449083773
|
click
|
accessibility
|
NULL
|
Find in Files
97 matches in 21 files
File mask:
*. Find in Files
97 matches in 21 files
File mask:
*.php
*.php
Auto
*.php
Filter Search Results
Pin Window
Search History
makeRequest
New Line
Match case
Words
Regex
Replace History
Replace
New Line
Preserve case
In Project
Module
Directory
Scope
Module
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot
/Users/lukas/jiminny/app/app/Exceptions
/Users/lukas/jiminny/app/app/Listeners/Crm
/Users/lukas/jiminny/app/app/Component/Queue/Job
/Users/lukas/jiminny/app/app/Listeners/AutomatedReports/UserPilot
/Users/lukas/jiminny/app/app/Events/Crm
/Users/lukas/jiminny/app/app/Jobs/AutomatedReports
/Users/lukas/jiminny/app/app/Listeners/Activities/Coaching/UserPilot
/Users/lukas/jiminny/app/app/Listeners/Activities/ActivityProvider/UserPilot
/Users/lukas/jiminny/app/app/Jobs/Activity/PushSummaryToCrm
/Users/lukas/jiminny/app/app/Repositories/Crm
/Users/lukas/jiminny/app/app/Jobs/Crm/Delete
/Users/lukas/jiminny/app/app/Services/Kiosk/AutomatedReports
/Users/lukas/jiminny/app/app/Http/Controllers/API/UserAutomatedReports
/Users/lukas/jiminny/app/app/Services/Crm/Salesforce
/Users/lukas/jiminny/app/app/Providers
/Users/lukas/jiminny/app/app/Services/Crm/IntegrationApp
/Users/lukas/jiminny/app/app/Events/Activities/Crm
/Users/lukas/jiminny/app/app/Listeners/Playbooks
/Users/lukas/jiminny/app/app/Console/Commands/Crm
/Users/lukas/jiminny/app/app/Services/Crm
/Users/lukas/jiminny/app/app/Http/Controllers
/Users/lukas/jiminny/app/app/Console/Commands/Reports
/Users/lukas/jiminny/app/app/VO/Repository/OnDemandActivitySearch
/Users/lukas/jiminny/app/app/Listeners/Activities/Conferences/UserPilot
/Users/lukas/jiminny/app/app/Http/Controllers/Webhook
/Users/lukas/jiminny/app/resources/views/emails/reports
/Users/lukas/jiminny/app/app/Mail/Reports
/Users/lukas/jiminny/app/app/Repositories
/Users/lukas/jiminny/app/app/Component/ActivitySearch/Service
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Salesforce
/Users/lukas/jiminny/app/routes
/Users/lukas/jiminny/app/app/Console/Commands
/Users/lukas/jiminny/app/database/migrations
/Users/lukas/Library/Application Support/JetBrains/PhpStorm2026.1/consoles/db/325d461a-c90f-430a-99d4-6ddfce0c61d7
/Users/lukas/jiminny/app/app/Http/Controllers/API/V2
/Users/lukas/jiminny/app/app/Jobs/Crm/Hubspot
/Users/lukas/jiminny/app/app/Component/DealInsights
/Users/lukas/jiminny/app/app/Policies
/Users/lukas/jiminny/app/app/Services/Crm/Helpers
/Users/lukas/jiminny/app/app/Jobs/Crm
/Users/lukas/jiminny/app/app/Models
/Users/lukas/jiminny/app/app/Listeners/Teams
/Users/lukas/jiminny/app/app/Jobs/Crm/Salesforce
/Users/lukas/jiminny/app/app
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/Journal
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/ServiceTraits
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/OpportunitySyncStrategy
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/Webhook
/Users/lukas/jiminny/app/storage/logs
/Users/lukas/jiminny/app
/Users/lukas/jiminny/app/app/Services/Internal
/Users/lukas/jiminny/app/app/Listeners/Transcription
/Users/lukas/jiminny/app/tests/Unit/Listeners/Teams
/Users/lukas/jiminny/app/app/Models/Crm
/Users/lukas/Library/Application Support/JetBrains/PhpStorm2026.1/consoles/db/91133dfa-8d71-4e12-bfb8-fec7f1afba8f
/Users/lukas/jiminny/app/app/Observers
/Users/lukas/jiminny/app/app/Services/Mail
/Users/lukas/jiminny/app/app/Console/Commands/Activities
/Users/lukas/jiminny/app/app/Console/Commands/Activities/Migrator
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot/ServiceTraits
/Users/lukas/jiminny/app/app/Jobs/User
/Users/lukas/jiminny/app/app/Models/Activity
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot/Webhook
/Users/lukas/jiminny/app/app/Component/AiAutomation/Listeners/PendingAnalysis
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot
/Users/lukas/jiminny/app/app/Component/ActivitySearch/FilterDefinition/DealInsights
/Users/lukas/jiminny/app/app/Services/Crm/DecorateActivity
/Users/lukas/jiminny/app/app/Component/Activity/Event
/Users/lukas/jiminny/app/app/Component/Sidekick
/Users/lukas/jiminny/app/app/Listeners/Activities/Conferences
/Users/lukas/jiminny/app/app/Listeners/Activities/Bots
/Users/lukas/jiminny/app/app/Services/RecallAI/Webhooks/Handlers
/Users/lukas/jiminny/app/app/Events/Activities/Bots
/Users/lukas/jiminny/app/app/Component/MeetingBot
/Users/lukas/jiminny/app/app/Services/Activity/RingCentral
/Users/lukas/jiminny/app/app/Http/Controllers/Webhook/Hubspot
/Users/lukas/jiminny/app/app/Services/Activity/Gmail
/Users/lukas/jiminny/app/app/Services/Crm/CrmObjects/ServiceTraits
/Users/lukas/jiminny/app/app/Jobs/Mailbox
/Users/lukas/jiminny/app/app/Console
/Users/lukas/jiminny/app/front-end/src/composables
/Users/lukas/jiminny/app/app/Console/Commands/Calendars
/Users/lukas/jiminny/app/app/Http/Controllers/API
/Users/lukas/jiminny/app/app/Http/Controllers/Internal/WebhookReceiver
/Users/lukas/jiminny/app/app/Services/Crm/IntegrationApp/ServiceTraits
/Users/lukas/jiminny/app/app/Component/Queue
/Users/lukas/jiminny/app/app/Console/Commands/Crm/Hubspot
/Users/lukas/jiminny/app/app/Component/Transcription/Job
/Users/lukas/jiminny/app/tests/Unit/Services/Listeners
/Users/lukas/jiminny/app/app/Services/Crm/Listeners
/Users/lukas/jiminny/app/app/Traits...
|
2863
|
NULL
|
NULL
|
NULL
|
|
2864
|
113
|
48
|
2026-05-07T11:43:44.741106+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778154224741_m1.jpg...
|
PhpStorm
|
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Find in Files
97 matches in 21 files
File mask:
*. Find in Files
97 matches in 21 files
File mask:
*.php
*.php
Auto
*.php
Filter Search Results
Pin Window
Search History
makeRequest
New Line
Match case
Words
Regex
Replace History
Replace
New Line
Preserve case
In Project
Module
Directory
Scope
Module
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot
/Users/lukas/jiminny/app/app/Exceptions
/Users/lukas/jiminny/app/app/Listeners/Crm
/Users/lukas/jiminny/app/app/Component/Queue/Job
/Users/lukas/jiminny/app/app/Listeners/AutomatedReports/UserPilot
/Users/lukas/jiminny/app/app/Events/Crm
/Users/lukas/jiminny/app/app/Jobs/AutomatedReports
/Users/lukas/jiminny/app/app/Listeners/Activities/Coaching/UserPilot
/Users/lukas/jiminny/app/app/Listeners/Activities/ActivityProvider/UserPilot
/Users/lukas/jiminny/app/app/Jobs/Activity/PushSummaryToCrm
/Users/lukas/jiminny/app/app/Repositories/Crm
/Users/lukas/jiminny/app/app/Jobs/Crm/Delete
/Users/lukas/jiminny/app/app/Services/Kiosk/AutomatedReports
/Users/lukas/jiminny/app/app/Http/Controllers/API/UserAutomatedReports
/Users/lukas/jiminny/app/app/Services/Crm/Salesforce
/Users/lukas/jiminny/app/app/Providers
/Users/lukas/jiminny/app/app/Services/Crm/IntegrationApp
/Users/lukas/jiminny/app/app/Events/Activities/Crm
/Users/lukas/jiminny/app/app/Listeners/Playbooks
/Users/lukas/jiminny/app/app/Console/Commands/Crm
/Users/lukas/jiminny/app/app/Services/Crm
/Users/lukas/jiminny/app/app/Http/Controllers
/Users/lukas/jiminny/app/app/Console/Commands/Reports
/Users/lukas/jiminny/app/app/VO/Repository/OnDemandActivitySearch
/Users/lukas/jiminny/app/app/Listeners/Activities/Conferences/UserPilot
/Users/lukas/jiminny/app/app/Http/Controllers/Webhook
/Users/lukas/jiminny/app/resources/views/emails/reports
/Users/lukas/jiminny/app/app/Mail/Reports
/Users/lukas/jiminny/app/app/Repositories
/Users/lukas/jiminny/app/app/Component/ActivitySearch/Service
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Salesforce
/Users/lukas/jiminny/app/routes
/Users/lukas/jiminny/app/app/Console/Commands
/Users/lukas/jiminny/app/database/migrations
/Users/lukas/Library/Application Support/JetBrains/PhpStorm2026.1/consoles/db/325d461a-c90f-430a-99d4-6ddfce0c61d7
/Users/lukas/jiminny/app/app/Http/Controllers/API/V2
/Users/lukas/jiminny/app/app/Jobs/Crm/Hubspot
/Users/lukas/jiminny/app/app/Component/DealInsights
/Users/lukas/jiminny/app/app/Policies
/Users/lukas/jiminny/app/app/Services/Crm/Helpers
/Users/lukas/jiminny/app/app/Jobs/Crm
/Users/lukas/jiminny/app/app/Models
/Users/lukas/jiminny/app/app/Listeners/Teams
/Users/lukas/jiminny/app/app/Jobs/Crm/Salesforce
/Users/lukas/jiminny/app/app
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/Journal
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/ServiceTraits
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/OpportunitySyncStrategy
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/Webhook
/Users/lukas/jiminny/app/storage/logs
/Users/lukas/jiminny/app
/Users/lukas/jiminny/app/app/Services/Internal
/Users/lukas/jiminny/app/app/Listeners/Transcription
/Users/lukas/jiminny/app/tests/Unit/Listeners/Teams
/Users/lukas/jiminny/app/app/Models/Crm
/Users/lukas/Library/Application Support/JetBrains/PhpStorm2026.1/consoles/db/91133dfa-8d71-4e12-bfb8-fec7f1afba8f
/Users/lukas/jiminny/app/app/Observers
/Users/lukas/jiminny/app/app/Services/Mail
/Users/lukas/jiminny/app/app/Console/Commands/Activities
/Users/lukas/jiminny/app/app/Console/Commands/Activities/Migrator
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot/ServiceTraits
/Users/lukas/jiminny/app/app/Jobs/User
/Users/lukas/jiminny/app/app/Models/Activity
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot/Webhook
/Users/lukas/jiminny/app/app/Component/AiAutomation/Listeners/PendingAnalysis
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot
/Users/lukas/jiminny/app/app/Component/ActivitySearch/FilterDefinition/DealInsights
/Users/lukas/jiminny/app/app/Services/Crm/DecorateActivity
/Users/lukas/jiminny/app/app/Component/Activity/Event
/Users/lukas/jiminny/app/app/Component/Sidekick
/Users/lukas/jiminny/app/app/Listeners/Activities/Conferences
/Users/lukas/jiminny/app/app/Listeners/Activities/Bots
/Users/lukas/jiminny/app/app/Services/RecallAI/Webhooks/Handlers
/Users/lukas/jiminny/app/app/Events/Activities/Bots
/Users/lukas/jiminny/app/app/Component/MeetingBot
/Users/lukas/jiminny/app/app/Services/Activity/RingCentral
/Users/lukas/jiminny/app/app/Http/Controllers/Webhook/Hubspot
/Users/lukas/jiminny/app/app/Services/Activity/Gmail
/Users/lukas/jiminny/app/app/Services/Crm/CrmObjects/ServiceTraits
/Users/lukas/jiminny/app/app/Jobs/Mailbox
/Users/lukas/jiminny/app/app/Console
/Users/lukas/jiminny/app/front-end/src/composables
/Users/lukas/jiminny/app/app/Console/Commands/Calendars
/Users/lukas/jiminny/app/app/Http/Controllers/API
/Users/lukas/jiminny/app/app/Http/Controllers/Internal/WebhookReceiver
/Users/lukas/jiminny/app/app/Services/Crm/IntegrationApp/ServiceTraits
/Users/lukas/jiminny/app/app/Component/Queue
/Users/lukas/jiminny/app/app/Console/Commands/Crm/Hubspot
/Users/lukas/jiminny/app/app/Component/Transcription/Job
/Users/lukas/jiminny/app/tests/Unit/Services/Listeners
/Users/lukas/jiminny/app/app/Services/Crm/Listeners
/Users/lukas/jiminny/app/app/Traits
/Users/lukas/jiminny/app/tests/Unit/Jobs/Crm/Hubspot
/Users/lukas/jiminny/app/tests/Unit/Services/Crm...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"Find in Files","depth":1,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"97 matches in 21 files","depth":1,"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"File mask:","depth":1,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXComboBox","text":"*.php","depth":1,"on_screen":true,"value":"*.php","role_description":"combo box","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"*.php","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Auto","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXTextField","text":"*.php","depth":2,"on_screen":true,"value":"*.php","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Filter Search Results","depth":1,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Pin Window","depth":1,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Search History","depth":1,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"makeRequest","depth":2,"on_screen":true,"value":"makeRequest","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"New Line","depth":1,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Match case","depth":1,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Words","depth":1,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Regex","depth":1,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Replace History","depth":1,"bounds":{"left":0.0,"top":0.0,"width":0.015277778,"height":0.024444444},"on_screen":false,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"Replace","depth":2,"on_screen":false,"role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"New Line","depth":1,"bounds":{"left":0.0,"top":0.0,"width":0.015277778,"height":0.024444444},"on_screen":false,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Preserve case","depth":1,"bounds":{"left":0.0,"top":0.0,"width":0.015277778,"height":0.024444444},"on_screen":false,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"In Project","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Module","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Directory","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Scope","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"Module","depth":1,"bounds":{"left":0.0,"top":0.0,"width":0.20833333,"height":0.037777778},"on_screen":false,"role_description":"pop up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXComboBox","text":"/Users/lukas/jiminny/app/app/Services/Crm/Hubspot","depth":1,"bounds":{"left":0.0,"top":0.0,"width":0.4125,"height":0.037777778},"on_screen":false,"value":"/Users/lukas/jiminny/app/app/Services/Crm/Hubspot","role_description":"combo box","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/Hubspot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Exceptions","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/Queue/Job","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/AutomatedReports/UserPilot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Events/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Jobs/AutomatedReports","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/Activities/Coaching/UserPilot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/Activities/ActivityProvider/UserPilot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Jobs/Activity/PushSummaryToCrm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Repositories/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Jobs/Crm/Delete","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Kiosk/AutomatedReports","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Http/Controllers/API/UserAutomatedReports","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/Salesforce","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Providers","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/IntegrationApp","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Events/Activities/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/Playbooks","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Console/Commands/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Http/Controllers","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Console/Commands/Reports","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/VO/Repository/OnDemandActivitySearch","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/Activities/Conferences/UserPilot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Http/Controllers/Webhook","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/resources/views/emails/reports","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Mail/Reports","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Repositories","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/ActivitySearch/Service","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Salesforce","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/routes","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Console/Commands","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/database/migrations","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/Library/Application Support/JetBrains/PhpStorm2026.1/consoles/db/325d461a-c90f-430a-99d4-6ddfce0c61d7","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Http/Controllers/API/V2","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Jobs/Crm/Hubspot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/DealInsights","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Policies","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/Helpers","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Jobs/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Models","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/Teams","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Jobs/Crm/Salesforce","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/Journal","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/ServiceTraits","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/OpportunitySyncStrategy","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/Webhook","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/storage/logs","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Internal","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/Transcription","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/tests/Unit/Listeners/Teams","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Models/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/Library/Application Support/JetBrains/PhpStorm2026.1/consoles/db/91133dfa-8d71-4e12-bfb8-fec7f1afba8f","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Observers","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Mail","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Console/Commands/Activities","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Console/Commands/Activities/Migrator","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot/ServiceTraits","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Jobs/User","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Models/Activity","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot/Webhook","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/AiAutomation/Listeners/PendingAnalysis","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/ActivitySearch/FilterDefinition/DealInsights","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/DecorateActivity","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/Activity/Event","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/Sidekick","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/Activities/Conferences","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/Activities/Bots","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/RecallAI/Webhooks/Handlers","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Events/Activities/Bots","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/MeetingBot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Activity/RingCentral","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Http/Controllers/Webhook/Hubspot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Activity/Gmail","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/CrmObjects/ServiceTraits","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Jobs/Mailbox","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Console","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/front-end/src/composables","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Console/Commands/Calendars","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Http/Controllers/API","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Http/Controllers/Internal/WebhookReceiver","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/IntegrationApp/ServiceTraits","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/Queue","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Console/Commands/Crm/Hubspot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/Transcription/Job","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/tests/Unit/Services/Listeners","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/Listeners","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Traits","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/tests/Unit/Jobs/Crm/Hubspot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/tests/Unit/Services/Crm","depth":6,"on_screen":false,"role_description":"text"}]...
|
-1639077626870265992
|
-440987284524939581
|
click
|
accessibility
|
NULL
|
Find in Files
97 matches in 21 files
File mask:
*. Find in Files
97 matches in 21 files
File mask:
*.php
*.php
Auto
*.php
Filter Search Results
Pin Window
Search History
makeRequest
New Line
Match case
Words
Regex
Replace History
Replace
New Line
Preserve case
In Project
Module
Directory
Scope
Module
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot
/Users/lukas/jiminny/app/app/Exceptions
/Users/lukas/jiminny/app/app/Listeners/Crm
/Users/lukas/jiminny/app/app/Component/Queue/Job
/Users/lukas/jiminny/app/app/Listeners/AutomatedReports/UserPilot
/Users/lukas/jiminny/app/app/Events/Crm
/Users/lukas/jiminny/app/app/Jobs/AutomatedReports
/Users/lukas/jiminny/app/app/Listeners/Activities/Coaching/UserPilot
/Users/lukas/jiminny/app/app/Listeners/Activities/ActivityProvider/UserPilot
/Users/lukas/jiminny/app/app/Jobs/Activity/PushSummaryToCrm
/Users/lukas/jiminny/app/app/Repositories/Crm
/Users/lukas/jiminny/app/app/Jobs/Crm/Delete
/Users/lukas/jiminny/app/app/Services/Kiosk/AutomatedReports
/Users/lukas/jiminny/app/app/Http/Controllers/API/UserAutomatedReports
/Users/lukas/jiminny/app/app/Services/Crm/Salesforce
/Users/lukas/jiminny/app/app/Providers
/Users/lukas/jiminny/app/app/Services/Crm/IntegrationApp
/Users/lukas/jiminny/app/app/Events/Activities/Crm
/Users/lukas/jiminny/app/app/Listeners/Playbooks
/Users/lukas/jiminny/app/app/Console/Commands/Crm
/Users/lukas/jiminny/app/app/Services/Crm
/Users/lukas/jiminny/app/app/Http/Controllers
/Users/lukas/jiminny/app/app/Console/Commands/Reports
/Users/lukas/jiminny/app/app/VO/Repository/OnDemandActivitySearch
/Users/lukas/jiminny/app/app/Listeners/Activities/Conferences/UserPilot
/Users/lukas/jiminny/app/app/Http/Controllers/Webhook
/Users/lukas/jiminny/app/resources/views/emails/reports
/Users/lukas/jiminny/app/app/Mail/Reports
/Users/lukas/jiminny/app/app/Repositories
/Users/lukas/jiminny/app/app/Component/ActivitySearch/Service
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Salesforce
/Users/lukas/jiminny/app/routes
/Users/lukas/jiminny/app/app/Console/Commands
/Users/lukas/jiminny/app/database/migrations
/Users/lukas/Library/Application Support/JetBrains/PhpStorm2026.1/consoles/db/325d461a-c90f-430a-99d4-6ddfce0c61d7
/Users/lukas/jiminny/app/app/Http/Controllers/API/V2
/Users/lukas/jiminny/app/app/Jobs/Crm/Hubspot
/Users/lukas/jiminny/app/app/Component/DealInsights
/Users/lukas/jiminny/app/app/Policies
/Users/lukas/jiminny/app/app/Services/Crm/Helpers
/Users/lukas/jiminny/app/app/Jobs/Crm
/Users/lukas/jiminny/app/app/Models
/Users/lukas/jiminny/app/app/Listeners/Teams
/Users/lukas/jiminny/app/app/Jobs/Crm/Salesforce
/Users/lukas/jiminny/app/app
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/Journal
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/ServiceTraits
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/OpportunitySyncStrategy
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/Webhook
/Users/lukas/jiminny/app/storage/logs
/Users/lukas/jiminny/app
/Users/lukas/jiminny/app/app/Services/Internal
/Users/lukas/jiminny/app/app/Listeners/Transcription
/Users/lukas/jiminny/app/tests/Unit/Listeners/Teams
/Users/lukas/jiminny/app/app/Models/Crm
/Users/lukas/Library/Application Support/JetBrains/PhpStorm2026.1/consoles/db/91133dfa-8d71-4e12-bfb8-fec7f1afba8f
/Users/lukas/jiminny/app/app/Observers
/Users/lukas/jiminny/app/app/Services/Mail
/Users/lukas/jiminny/app/app/Console/Commands/Activities
/Users/lukas/jiminny/app/app/Console/Commands/Activities/Migrator
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot/ServiceTraits
/Users/lukas/jiminny/app/app/Jobs/User
/Users/lukas/jiminny/app/app/Models/Activity
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot/Webhook
/Users/lukas/jiminny/app/app/Component/AiAutomation/Listeners/PendingAnalysis
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot
/Users/lukas/jiminny/app/app/Component/ActivitySearch/FilterDefinition/DealInsights
/Users/lukas/jiminny/app/app/Services/Crm/DecorateActivity
/Users/lukas/jiminny/app/app/Component/Activity/Event
/Users/lukas/jiminny/app/app/Component/Sidekick
/Users/lukas/jiminny/app/app/Listeners/Activities/Conferences
/Users/lukas/jiminny/app/app/Listeners/Activities/Bots
/Users/lukas/jiminny/app/app/Services/RecallAI/Webhooks/Handlers
/Users/lukas/jiminny/app/app/Events/Activities/Bots
/Users/lukas/jiminny/app/app/Component/MeetingBot
/Users/lukas/jiminny/app/app/Services/Activity/RingCentral
/Users/lukas/jiminny/app/app/Http/Controllers/Webhook/Hubspot
/Users/lukas/jiminny/app/app/Services/Activity/Gmail
/Users/lukas/jiminny/app/app/Services/Crm/CrmObjects/ServiceTraits
/Users/lukas/jiminny/app/app/Jobs/Mailbox
/Users/lukas/jiminny/app/app/Console
/Users/lukas/jiminny/app/front-end/src/composables
/Users/lukas/jiminny/app/app/Console/Commands/Calendars
/Users/lukas/jiminny/app/app/Http/Controllers/API
/Users/lukas/jiminny/app/app/Http/Controllers/Internal/WebhookReceiver
/Users/lukas/jiminny/app/app/Services/Crm/IntegrationApp/ServiceTraits
/Users/lukas/jiminny/app/app/Component/Queue
/Users/lukas/jiminny/app/app/Console/Commands/Crm/Hubspot
/Users/lukas/jiminny/app/app/Component/Transcription/Job
/Users/lukas/jiminny/app/tests/Unit/Services/Listeners
/Users/lukas/jiminny/app/app/Services/Crm/Listeners
/Users/lukas/jiminny/app/app/Traits
/Users/lukas/jiminny/app/tests/Unit/Jobs/Crm/Hubspot
/Users/lukas/jiminny/app/tests/Unit/Services/Crm...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
2863
|
114
|
47
|
2026-05-07T11:43:43.061326+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778154223061_m2.jpg...
|
PhpStorm
|
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Find in Files
97 matches in 21 files
File mask:
*. Find in Files
97 matches in 21 files
File mask:
*.php
*.php
Auto
*.php
Filter Search Results
Pin Window
Search History
makeRequest
New Line
Match case
Words
Regex
Replace History
Replace
New Line
Preserve case
In Project
Module
Directory
Scope
Module
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot
/Users/lukas/jiminny/app/app/Exceptions
/Users/lukas/jiminny/app/app/Listeners/Crm
/Users/lukas/jiminny/app/app/Component/Queue/Job
/Users/lukas/jiminny/app/app/Listeners/AutomatedReports/UserPilot
/Users/lukas/jiminny/app/app/Events/Crm
/Users/lukas/jiminny/app/app/Jobs/AutomatedReports
/Users/lukas/jiminny/app/app/Listeners/Activities/Coaching/UserPilot
/Users/lukas/jiminny/app/app/Listeners/Activities/ActivityProvider/UserPilot
/Users/lukas/jiminny/app/app/Jobs/Activity/PushSummaryToCrm
/Users/lukas/jiminny/app/app/Repositories/Crm
/Users/lukas/jiminny/app/app/Jobs/Crm/Delete
/Users/lukas/jiminny/app/app/Services/Kiosk/AutomatedReports
/Users/lukas/jiminny/app/app/Http/Controllers/API/UserAutomatedReports
/Users/lukas/jiminny/app/app/Services/Crm/Salesforce
/Users/lukas/jiminny/app/app/Providers
/Users/lukas/jiminny/app/app/Services/Crm/IntegrationApp
/Users/lukas/jiminny/app/app/Events/Activities/Crm
/Users/lukas/jiminny/app/app/Listeners/Playbooks
/Users/lukas/jiminny/app/app/Console/Commands/Crm
/Users/lukas/jiminny/app/app/Services/Crm
/Users/lukas/jiminny/app/app/Http/Controllers
/Users/lukas/jiminny/app/app/Console/Commands/Reports
/Users/lukas/jiminny/app/app/VO/Repository/OnDemandActivitySearch
/Users/lukas/jiminny/app/app/Listeners/Activities/Conferences/UserPilot
/Users/lukas/jiminny/app/app/Http/Controllers/Webhook
/Users/lukas/jiminny/app/resources/views/emails/reports
/Users/lukas/jiminny/app/app/Mail/Reports
/Users/lukas/jiminny/app/app/Repositories
/Users/lukas/jiminny/app/app/Component/ActivitySearch/Service
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Salesforce
/Users/lukas/jiminny/app/routes
/Users/lukas/jiminny/app/app/Console/Commands
/Users/lukas/jiminny/app/database/migrations
/Users/lukas/Library/Application Support/JetBrains/PhpStorm2026.1/consoles/db/325d461a-c90f-430a-99d4-6ddfce0c61d7
/Users/lukas/jiminny/app/app/Http/Controllers/API/V2
/Users/lukas/jiminny/app/app/Jobs/Crm/Hubspot
/Users/lukas/jiminny/app/app/Component/DealInsights
/Users/lukas/jiminny/app/app/Policies
/Users/lukas/jiminny/app/app/Services/Crm/Helpers
/Users/lukas/jiminny/app/app/Jobs/Crm
/Users/lukas/jiminny/app/app/Models
/Users/lukas/jiminny/app/app/Listeners/Teams
/Users/lukas/jiminny/app/app/Jobs/Crm/Salesforce
/Users/lukas/jiminny/app/app
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/Journal
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/ServiceTraits
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/OpportunitySyncStrategy
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/Webhook
/Users/lukas/jiminny/app/storage/logs
/Users/lukas/jiminny/app
/Users/lukas/jiminny/app/app/Services/Internal
/Users/lukas/jiminny/app/app/Listeners/Transcription
/Users/lukas/jiminny/app/tests/Unit/Listeners/Teams
/Users/lukas/jiminny/app/app/Models/Crm
/Users/lukas/Library/Application Support/JetBrains/PhpStorm2026.1/consoles/db/91133dfa-8d71-4e12-bfb8-fec7f1afba8f
/Users/lukas/jiminny/app/app/Observers
/Users/lukas/jiminny/app/app/Services/Mail
/Users/lukas/jiminny/app/app/Console/Commands/Activities
/Users/lukas/jiminny/app/app/Console/Commands/Activities/Migrator
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot/ServiceTraits
/Users/lukas/jiminny/app/app/Jobs/User
/Users/lukas/jiminny/app/app/Models/Activity
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot/Webhook
/Users/lukas/jiminny/app/app/Component/AiAutomation/Listeners/PendingAnalysis
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot
/Users/lukas/jiminny/app/app/Component/ActivitySearch/FilterDefinition/DealInsights
/Users/lukas/jiminny/app/app/Services/Crm/DecorateActivity
/Users/lukas/jiminny/app/app/Component/Activity/Event
/Users/lukas/jiminny/app/app/Component/Sidekick
/Users/lukas/jiminny/app/app/Listeners/Activities/Conferences
/Users/lukas/jiminny/app/app/Listeners/Activities/Bots
/Users/lukas/jiminny/app/app/Services/RecallAI/Webhooks/Handlers
/Users/lukas/jiminny/app/app/Events/Activities/Bots
/Users/lukas/jiminny/app/app/Component/MeetingBot
/Users/lukas/jiminny/app/app/Services/Activity/RingCentral
/Users/lukas/jiminny/app/app/Http/Controllers/Webhook/Hubspot
/Users/lukas/jiminny/app/app/Services/Activity/Gmail
/Users/lukas/jiminny/app/app/Services/Crm/CrmObjects/ServiceTraits
/Users/lukas/jiminny/app/app/Jobs/Mailbox
/Users/lukas/jiminny/app/app/Console
/Users/lukas/jiminny/app/front-end/src/composables
/Users/lukas/jiminny/app/app/Console/Commands/Calendars...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"Find in Files","depth":1,"bounds":{"left":0.2992021,"top":0.12609737,"width":0.024601065,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"97 matches in 21 files","depth":1,"bounds":{"left":0.32779256,"top":0.12609737,"width":0.044215426,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"File mask:","depth":1,"bounds":{"left":0.5315825,"top":0.12290503,"width":0.029587766,"height":0.019952115},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXComboBox","text":"*.php","depth":1,"bounds":{"left":0.5621675,"top":0.11971269,"width":0.027925532,"height":0.027134877},"on_screen":true,"value":"*.php","role_description":"combo box","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"*.php","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Auto","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXTextField","text":"*.php","depth":2,"bounds":{"left":0.5661569,"top":0.12609737,"width":0.011635638,"height":0.013567438},"on_screen":true,"value":"*.php","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Filter Search Results","depth":1,"bounds":{"left":0.5944149,"top":0.12290503,"width":0.00731383,"height":0.017557861},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Pin Window","depth":1,"bounds":{"left":0.6037234,"top":0.12290503,"width":0.00731383,"height":0.017557861},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Search History","depth":1,"bounds":{"left":0.2962101,"top":0.15403032,"width":0.00731383,"height":0.017557861},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"makeRequest","depth":2,"bounds":{"left":0.30718085,"top":0.15403032,"width":0.26196808,"height":0.017557861},"on_screen":true,"value":"makeRequest","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"New Line","depth":1,"bounds":{"left":0.578125,"top":0.15403032,"width":0.00731383,"height":0.017557861},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Match case","depth":1,"bounds":{"left":0.5880984,"top":0.15403032,"width":0.00731383,"height":0.017557861},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Words","depth":1,"bounds":{"left":0.59674203,"top":0.15403032,"width":0.00731383,"height":0.017557861},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Regex","depth":1,"bounds":{"left":0.60538566,"top":0.15403032,"width":0.00731383,"height":0.017557861},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Replace History","depth":1,"bounds":{"left":0.27027926,"top":1.0,"width":0.00731383,"height":0.0},"on_screen":false,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"Replace","depth":2,"on_screen":false,"role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"New Line","depth":1,"bounds":{"left":0.27027926,"top":1.0,"width":0.00731383,"height":0.0},"on_screen":false,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Preserve case","depth":1,"bounds":{"left":0.27027926,"top":1.0,"width":0.00731383,"height":0.0},"on_screen":false,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"In Project","depth":2,"bounds":{"left":0.2992021,"top":0.1867518,"width":0.022938829,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Module","depth":2,"bounds":{"left":0.32214096,"top":0.1867518,"width":0.019281914,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Directory","depth":2,"bounds":{"left":0.3414229,"top":0.1867518,"width":0.022606382,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Scope","depth":2,"bounds":{"left":0.36402926,"top":0.1867518,"width":0.017287234,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"Module","depth":1,"bounds":{"left":0.27027926,"top":1.0,"width":0.099734046,"height":0.0},"on_screen":false,"role_description":"pop up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXComboBox","text":"/Users/lukas/jiminny/app/app/Services/Crm/Hubspot","depth":1,"bounds":{"left":0.27027926,"top":1.0,"width":0.1974734,"height":0.0},"on_screen":false,"value":"/Users/lukas/jiminny/app/app/Services/Crm/Hubspot","role_description":"combo box","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/Hubspot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Exceptions","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/Queue/Job","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/AutomatedReports/UserPilot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Events/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Jobs/AutomatedReports","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/Activities/Coaching/UserPilot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/Activities/ActivityProvider/UserPilot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Jobs/Activity/PushSummaryToCrm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Repositories/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Jobs/Crm/Delete","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Kiosk/AutomatedReports","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Http/Controllers/API/UserAutomatedReports","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/Salesforce","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Providers","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/IntegrationApp","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Events/Activities/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/Playbooks","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Console/Commands/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Http/Controllers","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Console/Commands/Reports","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/VO/Repository/OnDemandActivitySearch","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/Activities/Conferences/UserPilot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Http/Controllers/Webhook","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/resources/views/emails/reports","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Mail/Reports","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Repositories","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/ActivitySearch/Service","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Salesforce","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/routes","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Console/Commands","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/database/migrations","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/Library/Application Support/JetBrains/PhpStorm2026.1/consoles/db/325d461a-c90f-430a-99d4-6ddfce0c61d7","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Http/Controllers/API/V2","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Jobs/Crm/Hubspot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/DealInsights","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Policies","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/Helpers","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Jobs/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Models","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/Teams","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Jobs/Crm/Salesforce","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/Journal","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/ServiceTraits","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/OpportunitySyncStrategy","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/Webhook","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/storage/logs","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Internal","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/Transcription","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/tests/Unit/Listeners/Teams","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Models/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/Library/Application Support/JetBrains/PhpStorm2026.1/consoles/db/91133dfa-8d71-4e12-bfb8-fec7f1afba8f","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Observers","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Mail","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Console/Commands/Activities","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Console/Commands/Activities/Migrator","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot/ServiceTraits","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Jobs/User","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Models/Activity","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot/Webhook","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/AiAutomation/Listeners/PendingAnalysis","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/ActivitySearch/FilterDefinition/DealInsights","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/DecorateActivity","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/Activity/Event","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/Sidekick","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/Activities/Conferences","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/Activities/Bots","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/RecallAI/Webhooks/Handlers","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Events/Activities/Bots","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/MeetingBot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Activity/RingCentral","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Http/Controllers/Webhook/Hubspot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Activity/Gmail","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/CrmObjects/ServiceTraits","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Jobs/Mailbox","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Console","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/front-end/src/composables","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Console/Commands/Calendars","depth":6,"on_screen":false,"role_description":"text"}]...
|
-366048596337455154
|
-2746689551745595709
|
click
|
accessibility
|
NULL
|
Find in Files
97 matches in 21 files
File mask:
*. Find in Files
97 matches in 21 files
File mask:
*.php
*.php
Auto
*.php
Filter Search Results
Pin Window
Search History
makeRequest
New Line
Match case
Words
Regex
Replace History
Replace
New Line
Preserve case
In Project
Module
Directory
Scope
Module
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot
/Users/lukas/jiminny/app/app/Exceptions
/Users/lukas/jiminny/app/app/Listeners/Crm
/Users/lukas/jiminny/app/app/Component/Queue/Job
/Users/lukas/jiminny/app/app/Listeners/AutomatedReports/UserPilot
/Users/lukas/jiminny/app/app/Events/Crm
/Users/lukas/jiminny/app/app/Jobs/AutomatedReports
/Users/lukas/jiminny/app/app/Listeners/Activities/Coaching/UserPilot
/Users/lukas/jiminny/app/app/Listeners/Activities/ActivityProvider/UserPilot
/Users/lukas/jiminny/app/app/Jobs/Activity/PushSummaryToCrm
/Users/lukas/jiminny/app/app/Repositories/Crm
/Users/lukas/jiminny/app/app/Jobs/Crm/Delete
/Users/lukas/jiminny/app/app/Services/Kiosk/AutomatedReports
/Users/lukas/jiminny/app/app/Http/Controllers/API/UserAutomatedReports
/Users/lukas/jiminny/app/app/Services/Crm/Salesforce
/Users/lukas/jiminny/app/app/Providers
/Users/lukas/jiminny/app/app/Services/Crm/IntegrationApp
/Users/lukas/jiminny/app/app/Events/Activities/Crm
/Users/lukas/jiminny/app/app/Listeners/Playbooks
/Users/lukas/jiminny/app/app/Console/Commands/Crm
/Users/lukas/jiminny/app/app/Services/Crm
/Users/lukas/jiminny/app/app/Http/Controllers
/Users/lukas/jiminny/app/app/Console/Commands/Reports
/Users/lukas/jiminny/app/app/VO/Repository/OnDemandActivitySearch
/Users/lukas/jiminny/app/app/Listeners/Activities/Conferences/UserPilot
/Users/lukas/jiminny/app/app/Http/Controllers/Webhook
/Users/lukas/jiminny/app/resources/views/emails/reports
/Users/lukas/jiminny/app/app/Mail/Reports
/Users/lukas/jiminny/app/app/Repositories
/Users/lukas/jiminny/app/app/Component/ActivitySearch/Service
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Salesforce
/Users/lukas/jiminny/app/routes
/Users/lukas/jiminny/app/app/Console/Commands
/Users/lukas/jiminny/app/database/migrations
/Users/lukas/Library/Application Support/JetBrains/PhpStorm2026.1/consoles/db/325d461a-c90f-430a-99d4-6ddfce0c61d7
/Users/lukas/jiminny/app/app/Http/Controllers/API/V2
/Users/lukas/jiminny/app/app/Jobs/Crm/Hubspot
/Users/lukas/jiminny/app/app/Component/DealInsights
/Users/lukas/jiminny/app/app/Policies
/Users/lukas/jiminny/app/app/Services/Crm/Helpers
/Users/lukas/jiminny/app/app/Jobs/Crm
/Users/lukas/jiminny/app/app/Models
/Users/lukas/jiminny/app/app/Listeners/Teams
/Users/lukas/jiminny/app/app/Jobs/Crm/Salesforce
/Users/lukas/jiminny/app/app
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/Journal
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/ServiceTraits
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/OpportunitySyncStrategy
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/Webhook
/Users/lukas/jiminny/app/storage/logs
/Users/lukas/jiminny/app
/Users/lukas/jiminny/app/app/Services/Internal
/Users/lukas/jiminny/app/app/Listeners/Transcription
/Users/lukas/jiminny/app/tests/Unit/Listeners/Teams
/Users/lukas/jiminny/app/app/Models/Crm
/Users/lukas/Library/Application Support/JetBrains/PhpStorm2026.1/consoles/db/91133dfa-8d71-4e12-bfb8-fec7f1afba8f
/Users/lukas/jiminny/app/app/Observers
/Users/lukas/jiminny/app/app/Services/Mail
/Users/lukas/jiminny/app/app/Console/Commands/Activities
/Users/lukas/jiminny/app/app/Console/Commands/Activities/Migrator
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot/ServiceTraits
/Users/lukas/jiminny/app/app/Jobs/User
/Users/lukas/jiminny/app/app/Models/Activity
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot/Webhook
/Users/lukas/jiminny/app/app/Component/AiAutomation/Listeners/PendingAnalysis
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot
/Users/lukas/jiminny/app/app/Component/ActivitySearch/FilterDefinition/DealInsights
/Users/lukas/jiminny/app/app/Services/Crm/DecorateActivity
/Users/lukas/jiminny/app/app/Component/Activity/Event
/Users/lukas/jiminny/app/app/Component/Sidekick
/Users/lukas/jiminny/app/app/Listeners/Activities/Conferences
/Users/lukas/jiminny/app/app/Listeners/Activities/Bots
/Users/lukas/jiminny/app/app/Services/RecallAI/Webhooks/Handlers
/Users/lukas/jiminny/app/app/Events/Activities/Bots
/Users/lukas/jiminny/app/app/Component/MeetingBot
/Users/lukas/jiminny/app/app/Services/Activity/RingCentral
/Users/lukas/jiminny/app/app/Http/Controllers/Webhook/Hubspot
/Users/lukas/jiminny/app/app/Services/Activity/Gmail
/Users/lukas/jiminny/app/app/Services/Crm/CrmObjects/ServiceTraits
/Users/lukas/jiminny/app/app/Jobs/Mailbox
/Users/lukas/jiminny/app/app/Console
/Users/lukas/jiminny/app/front-end/src/composables
/Users/lukas/jiminny/app/app/Console/Commands/Calendars...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
2862
|
113
|
47
|
2026-05-07T11:43:43.061286+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778154223061_m1.jpg...
|
PhpStorm
|
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Find in Files
97 matches in 21 files
File mask:
*. Find in Files
97 matches in 21 files
File mask:
*.php
*.php
Auto
*.php
Filter Search Results
Pin Window
Search History
makeRequest
New Line
Match case
Words
Regex
Replace History
Replace
New Line
Preserve case
In Project
Module
Directory...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"Find in Files","depth":1,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"97 matches in 21 files","depth":1,"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"File mask:","depth":1,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXComboBox","text":"*.php","depth":1,"on_screen":true,"value":"*.php","role_description":"combo box","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"*.php","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Auto","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXTextField","text":"*.php","depth":2,"on_screen":true,"value":"*.php","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Filter Search Results","depth":1,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Pin Window","depth":1,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Search History","depth":1,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"makeRequest","depth":2,"on_screen":true,"value":"makeRequest","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"New Line","depth":1,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Match case","depth":1,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Words","depth":1,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Regex","depth":1,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Replace History","depth":1,"bounds":{"left":0.0,"top":0.0,"width":0.015277778,"height":0.024444444},"on_screen":false,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"Replace","depth":2,"on_screen":false,"role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"New Line","depth":1,"bounds":{"left":0.0,"top":0.0,"width":0.015277778,"height":0.024444444},"on_screen":false,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Preserve case","depth":1,"bounds":{"left":0.0,"top":0.0,"width":0.015277778,"height":0.024444444},"on_screen":false,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"In Project","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Module","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Directory","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-2043019974611349268
|
8918090436357232203
|
click
|
accessibility
|
NULL
|
Find in Files
97 matches in 21 files
File mask:
*. Find in Files
97 matches in 21 files
File mask:
*.php
*.php
Auto
*.php
Filter Search Results
Pin Window
Search History
makeRequest
New Line
Match case
Words
Regex
Replace History
Replace
New Line
Preserve case
In Project
Module
Directory...
|
2860
|
NULL
|
NULL
|
NULL
|
|
2861
|
114
|
46
|
2026-05-07T11:43:41.620798+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778154221620_m2.jpg...
|
PhpStorm
|
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Find in Files
97 matches in 21 files
File mask:
*. Find in Files
97 matches in 21 files
File mask:
*.php
*.php
Auto
*.php
Filter Search Results
Pin Window
Search History
makeRequest
New Line
Match case
Words
Regex
Replace History
Replace
New Line
Preserve case
In Project
Module
Directory
Scope
Module
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot
/Users/lukas/jiminny/app/app/Exceptions
/Users/lukas/jiminny/app/app/Listeners/Crm
/Users/lukas/jiminny/app/app/Component/Queue/Job
/Users/lukas/jiminny/app/app/Listeners/AutomatedReports/UserPilot
/Users/lukas/jiminny/app/app/Events/Crm
/Users/lukas/jiminny/app/app/Jobs/AutomatedReports
/Users/lukas/jiminny/app/app/Listeners/Activities/Coaching/UserPilot
/Users/lukas/jiminny/app/app/Listeners/Activities/ActivityProvider/UserPilot
/Users/lukas/jiminny/app/app/Jobs/Activity/PushSummaryToCrm
/Users/lukas/jiminny/app/app/Repositories/Crm
/Users/lukas/jiminny/app/app/Jobs/Crm/Delete
/Users/lukas/jiminny/app/app/Services/Kiosk/AutomatedReports
/Users/lukas/jiminny/app/app/Http/Controllers/API/UserAutomatedReports
/Users/lukas/jiminny/app/app/Services/Crm/Salesforce
/Users/lukas/jiminny/app/app/Providers
/Users/lukas/jiminny/app/app/Services/Crm/IntegrationApp
/Users/lukas/jiminny/app/app/Events/Activities/Crm
/Users/lukas/jiminny/app/app/Listeners/Playbooks
/Users/lukas/jiminny/app/app/Console/Commands/Crm
/Users/lukas/jiminny/app/app/Services/Crm
/Users/lukas/jiminny/app/app/Http/Controllers
/Users/lukas/jiminny/app/app/Console/Commands/Reports
/Users/lukas/jiminny/app/app/VO/Repository/OnDemandActivitySearch
/Users/lukas/jiminny/app/app/Listeners/Activities/Conferences/UserPilot
/Users/lukas/jiminny/app/app/Http/Controllers/Webhook
/Users/lukas/jiminny/app/resources/views/emails/reports
/Users/lukas/jiminny/app/app/Mail/Reports
/Users/lukas/jiminny/app/app/Repositories
/Users/lukas/jiminny/app/app/Component/ActivitySearch/Service
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Salesforce
/Users/lukas/jiminny/app/routes
/Users/lukas/jiminny/app/app/Console/Commands
/Users/lukas/jiminny/app/database/migrations
/Users/lukas/Library/Application Support/JetBrains/PhpStorm2026.1/consoles/db/325d461a-c90f-430a-99d4-6ddfce0c61d7
/Users/lukas/jiminny/app/app/Http/Controllers/API/V2
/Users/lukas/jiminny/app/app/Jobs/Crm/Hubspot
/Users/lukas/jiminny/app/app/Component/DealInsights
/Users/lukas/jiminny/app/app/Policies
/Users/lukas/jiminny/app/app/Services/Crm/Helpers
/Users/lukas/jiminny/app/app/Jobs/Crm
/Users/lukas/jiminny/app/app/Models
/Users/lukas/jiminny/app/app/Listeners/Teams
/Users/lukas/jiminny/app/app/Jobs/Crm/Salesforce
/Users/lukas/jiminny/app/app
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/Journal
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/ServiceTraits
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/OpportunitySyncStrategy
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/Webhook
/Users/lukas/jiminny/app/storage/logs
/Users/lukas/jiminny/app
/Users/lukas/jiminny/app/app/Services/Internal
/Users/lukas/jiminny/app/app/Listeners/Transcription
/Users/lukas/jiminny/app/tests/Unit/Listeners/Teams
/Users/lukas/jiminny/app/app/Models/Crm
/Users/lukas/Library/Application Support/JetBrains/PhpStorm2026.1/consoles/db/91133dfa-8d71-4e12-bfb8-fec7f1afba8f
/Users/lukas/jiminny/app/app/Observers
/Users/lukas/jiminny/app/app/Services/Mail
/Users/lukas/jiminny/app/app/Console/Commands/Activities
/Users/lukas/jiminny/app/app/Console/Commands/Activities/Migrator
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot/ServiceTraits
/Users/lukas/jiminny/app/app/Jobs/User
/Users/lukas/jiminny/app/app/Models/Activity
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot/Webhook
/Users/lukas/jiminny/app/app/Component/AiAutomation/Listeners/PendingAnalysis
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot
/Users/lukas/jiminny/app/app/Component/ActivitySearch/FilterDefinition/DealInsights
/Users/lukas/jiminny/app/app/Services/Crm/DecorateActivity
/Users/lukas/jiminny/app/app/Component/Activity/Event
/Users/lukas/jiminny/app/app/Component/Sidekick
/Users/lukas/jiminny/app/app/Listeners/Activities/Conferences
/Users/lukas/jiminny/app/app/Listeners/Activities/Bots
/Users/lukas/jiminny/app/app/Services/RecallAI/Webhooks/Handlers
/Users/lukas/jiminny/app/app/Events/Activities/Bots
/Users/lukas/jiminny/app/app/Component/MeetingBot
/Users/lukas/jiminny/app/app/Services/Activity/RingCentral
/Users/lukas/jiminny/app/app/Http/Controllers/Webhook/Hubspot
/Users/lukas/jiminny/app/app/Services/Activity/Gmail
/Users/lukas/jiminny/app/app/Services/Crm/CrmObjects/ServiceTraits
/Users/lukas/jiminny/app/app/Jobs/Mailbox
/Users/lukas/jiminny/app/app/Console
/Users/lukas/jiminny/app/front-end/src/composables
/Users/lukas/jiminny/app/app/Console/Commands/Calendars
/Users/lukas/jiminny/app/app/Http/Controllers/API
/Users/lukas/jiminny/app/app/Http/Controllers/Internal/WebhookReceiver
/Users/lukas/jiminny/app/app/Services/Crm/IntegrationApp/ServiceTraits
/Users/lukas/jiminny/app/app/Component/Queue
/Users/lukas/jiminny/app/app/Console/Commands/Crm/Hubspot
/Users/lukas/jiminny/app/app/Component/Transcription/Job
/Users/lukas/jiminny/app/tests/Unit/Services/Listeners
/Users/lukas/jiminny/app/app/Services/Crm/Listeners
/Users/lukas/jiminny/app/app/Traits
/Users/lukas/jiminny/app/tests/Unit/Jobs/Crm/Hubspot
/Users/lukas/jiminny/app/tests/Unit/Services/Crm
/Users/lukas/jiminny/app/app/Services/Activity
/Users/lukas/jiminny/app/app/Services/Calendar/Command
/Users/lukas/jiminny/app/.idea/queries
/Users/lukas/jiminny/app/vendor/hubspot/api-client/codegen/Crm
/Users/lukas/jiminny/app/vendor/hubspot
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/Fields
/Users/lukas/jiminny/app/app/Services/Crm/Copper
/Users/lukas/jiminny/app/app/Services/Crm/Bullhorn
/Users/lukas/jiminny/app/app/Notifications/Channels
/Users/lukas/jiminny/app/tests/Unit
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot/Journal
/Users/lukas/jiminny/app/app/Interactions/Settings/Teams
/Users/lukas/jiminny/app/app/Exceptions/Crm
/Users/lukas/jiminny/app/vendor/hubspot/hubspot-php/src/Endpoints
/Users/lukas/jiminny/app/config
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot/OpportunitySyncStrategy
/Users/lukas/jiminny/app/vendor/laravel/framework/src/Illuminate/Redis/Connections
/Users/lukas/jiminny/app/app/Http/Controllers/Settings/Teams
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/Webhook/Traits
/Users/lukas/jiminny/app/vendor/laravel/framework/src/Illuminate/Broadcasting
/Users/lukas/jiminny/app/app/Component/FeatureFlags
/Users/lukas/jiminny/app/app/Component/Activity
/Users/lukas/jiminny/app/app/Component/ActivitySearch
/Users/lukas/jiminny/app/tests/Unit/Events/Activities/Crm
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot/Pagination
/Users/lukas/jiminny/app/app/Console/Commands/Dev
/Users/lukas/jiminny/app/front-end
/Users/lukas/jiminny/app/app/Component/Prophet
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/IntegrationApp
/Users/lukas/jiminny/app/app/Component/AskAnything
/Users/lukas/jiminny/app/app/Component/AskJiminnyAi/OnDemandLevel/Events
/Users/lukas/jiminny/app/app/Component/AskAnything/Events
/Users/lukas/jiminny/app/app/Component/AskJiminnyAi/DealLevel/Traits
/Users/lukas/jiminny/app/app/Component/ProphetAi...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"Find in Files","depth":1,"bounds":{"left":0.2992021,"top":0.12609737,"width":0.024601065,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"97 matches in 21 files","depth":1,"bounds":{"left":0.32779256,"top":0.12609737,"width":0.044215426,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"File mask:","depth":1,"bounds":{"left":0.5315825,"top":0.12290503,"width":0.029587766,"height":0.019952115},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXComboBox","text":"*.php","depth":1,"bounds":{"left":0.5621675,"top":0.11971269,"width":0.027925532,"height":0.027134877},"on_screen":true,"value":"*.php","role_description":"combo box","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"*.php","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Auto","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXTextField","text":"*.php","depth":2,"bounds":{"left":0.5661569,"top":0.12609737,"width":0.011635638,"height":0.013567438},"on_screen":true,"value":"*.php","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Filter Search Results","depth":1,"bounds":{"left":0.5944149,"top":0.12290503,"width":0.00731383,"height":0.017557861},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Pin Window","depth":1,"bounds":{"left":0.6037234,"top":0.12290503,"width":0.00731383,"height":0.017557861},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Search History","depth":1,"bounds":{"left":0.2962101,"top":0.15403032,"width":0.00731383,"height":0.017557861},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"makeRequest","depth":2,"bounds":{"left":0.30718085,"top":0.15403032,"width":0.26196808,"height":0.017557861},"on_screen":true,"value":"makeRequest","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"New Line","depth":1,"bounds":{"left":0.578125,"top":0.15403032,"width":0.00731383,"height":0.017557861},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Match case","depth":1,"bounds":{"left":0.5880984,"top":0.15403032,"width":0.00731383,"height":0.017557861},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Words","depth":1,"bounds":{"left":0.59674203,"top":0.15403032,"width":0.00731383,"height":0.017557861},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Regex","depth":1,"bounds":{"left":0.60538566,"top":0.15403032,"width":0.00731383,"height":0.017557861},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Replace History","depth":1,"bounds":{"left":0.27027926,"top":1.0,"width":0.00731383,"height":0.0},"on_screen":false,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"Replace","depth":2,"on_screen":false,"role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"New Line","depth":1,"bounds":{"left":0.27027926,"top":1.0,"width":0.00731383,"height":0.0},"on_screen":false,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Preserve case","depth":1,"bounds":{"left":0.27027926,"top":1.0,"width":0.00731383,"height":0.0},"on_screen":false,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"In Project","depth":2,"bounds":{"left":0.2992021,"top":0.1867518,"width":0.022938829,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Module","depth":2,"bounds":{"left":0.32214096,"top":0.1867518,"width":0.019281914,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Directory","depth":2,"bounds":{"left":0.3414229,"top":0.1867518,"width":0.022606382,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Scope","depth":2,"bounds":{"left":0.36402926,"top":0.1867518,"width":0.017287234,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"Module","depth":1,"bounds":{"left":0.27027926,"top":1.0,"width":0.099734046,"height":0.0},"on_screen":false,"role_description":"pop up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXComboBox","text":"/Users/lukas/jiminny/app/app/Services/Crm/Hubspot","depth":1,"bounds":{"left":0.27027926,"top":1.0,"width":0.1974734,"height":0.0},"on_screen":false,"value":"/Users/lukas/jiminny/app/app/Services/Crm/Hubspot","role_description":"combo box","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/Hubspot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Exceptions","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/Queue/Job","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/AutomatedReports/UserPilot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Events/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Jobs/AutomatedReports","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/Activities/Coaching/UserPilot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/Activities/ActivityProvider/UserPilot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Jobs/Activity/PushSummaryToCrm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Repositories/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Jobs/Crm/Delete","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Kiosk/AutomatedReports","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Http/Controllers/API/UserAutomatedReports","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/Salesforce","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Providers","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/IntegrationApp","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Events/Activities/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/Playbooks","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Console/Commands/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Http/Controllers","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Console/Commands/Reports","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/VO/Repository/OnDemandActivitySearch","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/Activities/Conferences/UserPilot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Http/Controllers/Webhook","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/resources/views/emails/reports","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Mail/Reports","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Repositories","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/ActivitySearch/Service","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Salesforce","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/routes","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Console/Commands","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/database/migrations","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/Library/Application Support/JetBrains/PhpStorm2026.1/consoles/db/325d461a-c90f-430a-99d4-6ddfce0c61d7","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Http/Controllers/API/V2","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Jobs/Crm/Hubspot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/DealInsights","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Policies","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/Helpers","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Jobs/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Models","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/Teams","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Jobs/Crm/Salesforce","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/Journal","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/ServiceTraits","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/OpportunitySyncStrategy","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/Webhook","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/storage/logs","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Internal","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/Transcription","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/tests/Unit/Listeners/Teams","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Models/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/Library/Application Support/JetBrains/PhpStorm2026.1/consoles/db/91133dfa-8d71-4e12-bfb8-fec7f1afba8f","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Observers","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Mail","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Console/Commands/Activities","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Console/Commands/Activities/Migrator","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot/ServiceTraits","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Jobs/User","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Models/Activity","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot/Webhook","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/AiAutomation/Listeners/PendingAnalysis","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/ActivitySearch/FilterDefinition/DealInsights","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/DecorateActivity","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/Activity/Event","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/Sidekick","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/Activities/Conferences","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/Activities/Bots","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/RecallAI/Webhooks/Handlers","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Events/Activities/Bots","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/MeetingBot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Activity/RingCentral","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Http/Controllers/Webhook/Hubspot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Activity/Gmail","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/CrmObjects/ServiceTraits","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Jobs/Mailbox","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Console","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/front-end/src/composables","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Console/Commands/Calendars","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Http/Controllers/API","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Http/Controllers/Internal/WebhookReceiver","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/IntegrationApp/ServiceTraits","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/Queue","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Console/Commands/Crm/Hubspot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/Transcription/Job","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/tests/Unit/Services/Listeners","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/Listeners","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Traits","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/tests/Unit/Jobs/Crm/Hubspot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/tests/Unit/Services/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Activity","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Calendar/Command","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/.idea/queries","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/vendor/hubspot/api-client/codegen/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/vendor/hubspot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/Fields","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/Copper","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/Bullhorn","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Notifications/Channels","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/tests/Unit","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot/Journal","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Interactions/Settings/Teams","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Exceptions/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/vendor/hubspot/hubspot-php/src/Endpoints","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/config","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot/OpportunitySyncStrategy","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/vendor/laravel/framework/src/Illuminate/Redis/Connections","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Http/Controllers/Settings/Teams","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/Webhook/Traits","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/vendor/laravel/framework/src/Illuminate/Broadcasting","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/FeatureFlags","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/Activity","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/ActivitySearch","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/tests/Unit/Events/Activities/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot/Pagination","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Console/Commands/Dev","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/front-end","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/Prophet","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/tests/Unit/Services/Crm/IntegrationApp","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/AskAnything","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/AskJiminnyAi/OnDemandLevel/Events","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/AskAnything/Events","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/AskJiminnyAi/DealLevel/Traits","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/ProphetAi","depth":6,"on_screen":false,"role_description":"text"}]...
|
-4681725591604833536
|
-294057363795253693
|
click
|
accessibility
|
NULL
|
Find in Files
97 matches in 21 files
File mask:
*. Find in Files
97 matches in 21 files
File mask:
*.php
*.php
Auto
*.php
Filter Search Results
Pin Window
Search History
makeRequest
New Line
Match case
Words
Regex
Replace History
Replace
New Line
Preserve case
In Project
Module
Directory
Scope
Module
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot
/Users/lukas/jiminny/app/app/Exceptions
/Users/lukas/jiminny/app/app/Listeners/Crm
/Users/lukas/jiminny/app/app/Component/Queue/Job
/Users/lukas/jiminny/app/app/Listeners/AutomatedReports/UserPilot
/Users/lukas/jiminny/app/app/Events/Crm
/Users/lukas/jiminny/app/app/Jobs/AutomatedReports
/Users/lukas/jiminny/app/app/Listeners/Activities/Coaching/UserPilot
/Users/lukas/jiminny/app/app/Listeners/Activities/ActivityProvider/UserPilot
/Users/lukas/jiminny/app/app/Jobs/Activity/PushSummaryToCrm
/Users/lukas/jiminny/app/app/Repositories/Crm
/Users/lukas/jiminny/app/app/Jobs/Crm/Delete
/Users/lukas/jiminny/app/app/Services/Kiosk/AutomatedReports
/Users/lukas/jiminny/app/app/Http/Controllers/API/UserAutomatedReports
/Users/lukas/jiminny/app/app/Services/Crm/Salesforce
/Users/lukas/jiminny/app/app/Providers
/Users/lukas/jiminny/app/app/Services/Crm/IntegrationApp
/Users/lukas/jiminny/app/app/Events/Activities/Crm
/Users/lukas/jiminny/app/app/Listeners/Playbooks
/Users/lukas/jiminny/app/app/Console/Commands/Crm
/Users/lukas/jiminny/app/app/Services/Crm
/Users/lukas/jiminny/app/app/Http/Controllers
/Users/lukas/jiminny/app/app/Console/Commands/Reports
/Users/lukas/jiminny/app/app/VO/Repository/OnDemandActivitySearch
/Users/lukas/jiminny/app/app/Listeners/Activities/Conferences/UserPilot
/Users/lukas/jiminny/app/app/Http/Controllers/Webhook
/Users/lukas/jiminny/app/resources/views/emails/reports
/Users/lukas/jiminny/app/app/Mail/Reports
/Users/lukas/jiminny/app/app/Repositories
/Users/lukas/jiminny/app/app/Component/ActivitySearch/Service
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Salesforce
/Users/lukas/jiminny/app/routes
/Users/lukas/jiminny/app/app/Console/Commands
/Users/lukas/jiminny/app/database/migrations
/Users/lukas/Library/Application Support/JetBrains/PhpStorm2026.1/consoles/db/325d461a-c90f-430a-99d4-6ddfce0c61d7
/Users/lukas/jiminny/app/app/Http/Controllers/API/V2
/Users/lukas/jiminny/app/app/Jobs/Crm/Hubspot
/Users/lukas/jiminny/app/app/Component/DealInsights
/Users/lukas/jiminny/app/app/Policies
/Users/lukas/jiminny/app/app/Services/Crm/Helpers
/Users/lukas/jiminny/app/app/Jobs/Crm
/Users/lukas/jiminny/app/app/Models
/Users/lukas/jiminny/app/app/Listeners/Teams
/Users/lukas/jiminny/app/app/Jobs/Crm/Salesforce
/Users/lukas/jiminny/app/app
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/Journal
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/ServiceTraits
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/OpportunitySyncStrategy
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/Webhook
/Users/lukas/jiminny/app/storage/logs
/Users/lukas/jiminny/app
/Users/lukas/jiminny/app/app/Services/Internal
/Users/lukas/jiminny/app/app/Listeners/Transcription
/Users/lukas/jiminny/app/tests/Unit/Listeners/Teams
/Users/lukas/jiminny/app/app/Models/Crm
/Users/lukas/Library/Application Support/JetBrains/PhpStorm2026.1/consoles/db/91133dfa-8d71-4e12-bfb8-fec7f1afba8f
/Users/lukas/jiminny/app/app/Observers
/Users/lukas/jiminny/app/app/Services/Mail
/Users/lukas/jiminny/app/app/Console/Commands/Activities
/Users/lukas/jiminny/app/app/Console/Commands/Activities/Migrator
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot/ServiceTraits
/Users/lukas/jiminny/app/app/Jobs/User
/Users/lukas/jiminny/app/app/Models/Activity
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot/Webhook
/Users/lukas/jiminny/app/app/Component/AiAutomation/Listeners/PendingAnalysis
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot
/Users/lukas/jiminny/app/app/Component/ActivitySearch/FilterDefinition/DealInsights
/Users/lukas/jiminny/app/app/Services/Crm/DecorateActivity
/Users/lukas/jiminny/app/app/Component/Activity/Event
/Users/lukas/jiminny/app/app/Component/Sidekick
/Users/lukas/jiminny/app/app/Listeners/Activities/Conferences
/Users/lukas/jiminny/app/app/Listeners/Activities/Bots
/Users/lukas/jiminny/app/app/Services/RecallAI/Webhooks/Handlers
/Users/lukas/jiminny/app/app/Events/Activities/Bots
/Users/lukas/jiminny/app/app/Component/MeetingBot
/Users/lukas/jiminny/app/app/Services/Activity/RingCentral
/Users/lukas/jiminny/app/app/Http/Controllers/Webhook/Hubspot
/Users/lukas/jiminny/app/app/Services/Activity/Gmail
/Users/lukas/jiminny/app/app/Services/Crm/CrmObjects/ServiceTraits
/Users/lukas/jiminny/app/app/Jobs/Mailbox
/Users/lukas/jiminny/app/app/Console
/Users/lukas/jiminny/app/front-end/src/composables
/Users/lukas/jiminny/app/app/Console/Commands/Calendars
/Users/lukas/jiminny/app/app/Http/Controllers/API
/Users/lukas/jiminny/app/app/Http/Controllers/Internal/WebhookReceiver
/Users/lukas/jiminny/app/app/Services/Crm/IntegrationApp/ServiceTraits
/Users/lukas/jiminny/app/app/Component/Queue
/Users/lukas/jiminny/app/app/Console/Commands/Crm/Hubspot
/Users/lukas/jiminny/app/app/Component/Transcription/Job
/Users/lukas/jiminny/app/tests/Unit/Services/Listeners
/Users/lukas/jiminny/app/app/Services/Crm/Listeners
/Users/lukas/jiminny/app/app/Traits
/Users/lukas/jiminny/app/tests/Unit/Jobs/Crm/Hubspot
/Users/lukas/jiminny/app/tests/Unit/Services/Crm
/Users/lukas/jiminny/app/app/Services/Activity
/Users/lukas/jiminny/app/app/Services/Calendar/Command
/Users/lukas/jiminny/app/.idea/queries
/Users/lukas/jiminny/app/vendor/hubspot/api-client/codegen/Crm
/Users/lukas/jiminny/app/vendor/hubspot
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/Fields
/Users/lukas/jiminny/app/app/Services/Crm/Copper
/Users/lukas/jiminny/app/app/Services/Crm/Bullhorn
/Users/lukas/jiminny/app/app/Notifications/Channels
/Users/lukas/jiminny/app/tests/Unit
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot/Journal
/Users/lukas/jiminny/app/app/Interactions/Settings/Teams
/Users/lukas/jiminny/app/app/Exceptions/Crm
/Users/lukas/jiminny/app/vendor/hubspot/hubspot-php/src/Endpoints
/Users/lukas/jiminny/app/config
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot/OpportunitySyncStrategy
/Users/lukas/jiminny/app/vendor/laravel/framework/src/Illuminate/Redis/Connections
/Users/lukas/jiminny/app/app/Http/Controllers/Settings/Teams
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/Webhook/Traits
/Users/lukas/jiminny/app/vendor/laravel/framework/src/Illuminate/Broadcasting
/Users/lukas/jiminny/app/app/Component/FeatureFlags
/Users/lukas/jiminny/app/app/Component/Activity
/Users/lukas/jiminny/app/app/Component/ActivitySearch
/Users/lukas/jiminny/app/tests/Unit/Events/Activities/Crm
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot/Pagination
/Users/lukas/jiminny/app/app/Console/Commands/Dev
/Users/lukas/jiminny/app/front-end
/Users/lukas/jiminny/app/app/Component/Prophet
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/IntegrationApp
/Users/lukas/jiminny/app/app/Component/AskAnything
/Users/lukas/jiminny/app/app/Component/AskJiminnyAi/OnDemandLevel/Events
/Users/lukas/jiminny/app/app/Component/AskAnything/Events
/Users/lukas/jiminny/app/app/Component/AskJiminnyAi/DealLevel/Traits
/Users/lukas/jiminny/app/app/Component/ProphetAi...
|
2859
|
NULL
|
NULL
|
NULL
|
|
2860
|
113
|
46
|
2026-05-07T11:43:41.620817+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778154221620_m1.jpg...
|
PhpStorm
|
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Find in Files
97 matches in 21 files
File mask:
*. Find in Files
97 matches in 21 files
File mask:
*.php
*.php
Auto
*.php
Filter Search Results
Pin Window
Search History
makeRequest
New Line
Match case
Words
Regex
Replace History
Replace
New Line
Preserve case
In Project
Module
Directory
Scope
Module
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot
/Users/lukas/jiminny/app/app/Exceptions
/Users/lukas/jiminny/app/app/Listeners/Crm
/Users/lukas/jiminny/app/app/Component/Queue/Job
/Users/lukas/jiminny/app/app/Listeners/AutomatedReports/UserPilot
/Users/lukas/jiminny/app/app/Events/Crm
/Users/lukas/jiminny/app/app/Jobs/AutomatedReports
/Users/lukas/jiminny/app/app/Listeners/Activities/Coaching/UserPilot
/Users/lukas/jiminny/app/app/Listeners/Activities/ActivityProvider/UserPilot
/Users/lukas/jiminny/app/app/Jobs/Activity/PushSummaryToCrm
/Users/lukas/jiminny/app/app/Repositories/Crm
/Users/lukas/jiminny/app/app/Jobs/Crm/Delete
/Users/lukas/jiminny/app/app/Services/Kiosk/AutomatedReports
/Users/lukas/jiminny/app/app/Http/Controllers/API/UserAutomatedReports
/Users/lukas/jiminny/app/app/Services/Crm/Salesforce
/Users/lukas/jiminny/app/app/Providers
/Users/lukas/jiminny/app/app/Services/Crm/IntegrationApp
/Users/lukas/jiminny/app/app/Events/Activities/Crm
/Users/lukas/jiminny/app/app/Listeners/Playbooks
/Users/lukas/jiminny/app/app/Console/Commands/Crm
/Users/lukas/jiminny/app/app/Services/Crm
/Users/lukas/jiminny/app/app/Http/Controllers
/Users/lukas/jiminny/app/app/Console/Commands/Reports
/Users/lukas/jiminny/app/app/VO/Repository/OnDemandActivitySearch
/Users/lukas/jiminny/app/app/Listeners/Activities/Conferences/UserPilot
/Users/lukas/jiminny/app/app/Http/Controllers/Webhook
/Users/lukas/jiminny/app/resources/views/emails/reports
/Users/lukas/jiminny/app/app/Mail/Reports
/Users/lukas/jiminny/app/app/Repositories
/Users/lukas/jiminny/app/app/Component/ActivitySearch/Service
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Salesforce
/Users/lukas/jiminny/app/routes
/Users/lukas/jiminny/app/app/Console/Commands
/Users/lukas/jiminny/app/database/migrations
/Users/lukas/Library/Application Support/JetBrains/PhpStorm2026.1/consoles/db/325d461a-c90f-430a-99d4-6ddfce0c61d7
/Users/lukas/jiminny/app/app/Http/Controllers/API/V2
/Users/lukas/jiminny/app/app/Jobs/Crm/Hubspot
/Users/lukas/jiminny/app/app/Component/DealInsights
/Users/lukas/jiminny/app/app/Policies
/Users/lukas/jiminny/app/app/Services/Crm/Helpers
/Users/lukas/jiminny/app/app/Jobs/Crm
/Users/lukas/jiminny/app/app/Models
/Users/lukas/jiminny/app/app/Listeners/Teams
/Users/lukas/jiminny/app/app/Jobs/Crm/Salesforce
/Users/lukas/jiminny/app/app
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/Journal
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/ServiceTraits
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/OpportunitySyncStrategy
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/Webhook
/Users/lukas/jiminny/app/storage/logs
/Users/lukas/jiminny/app
/Users/lukas/jiminny/app/app/Services/Internal
/Users/lukas/jiminny/app/app/Listeners/Transcription
/Users/lukas/jiminny/app/tests/Unit/Listeners/Teams
/Users/lukas/jiminny/app/app/Models/Crm
/Users/lukas/Library/Application Support/JetBrains/PhpStorm2026.1/consoles/db/91133dfa-8d71-4e12-bfb8-fec7f1afba8f
/Users/lukas/jiminny/app/app/Observers
/Users/lukas/jiminny/app/app/Services/Mail
/Users/lukas/jiminny/app/app/Console/Commands/Activities
/Users/lukas/jiminny/app/app/Console/Commands/Activities/Migrator
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot/ServiceTraits
/Users/lukas/jiminny/app/app/Jobs/User
/Users/lukas/jiminny/app/app/Models/Activity
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot/Webhook
/Users/lukas/jiminny/app/app/Component/AiAutomation/Listeners/PendingAnalysis...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"Find in Files","depth":1,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"97 matches in 21 files","depth":1,"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"File mask:","depth":1,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXComboBox","text":"*.php","depth":1,"on_screen":true,"value":"*.php","role_description":"combo box","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"*.php","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Auto","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXTextField","text":"*.php","depth":2,"on_screen":true,"value":"*.php","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Filter Search Results","depth":1,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Pin Window","depth":1,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Search History","depth":1,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"makeRequest","depth":2,"on_screen":true,"value":"makeRequest","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"New Line","depth":1,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Match case","depth":1,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Words","depth":1,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Regex","depth":1,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Replace History","depth":1,"bounds":{"left":0.0,"top":0.0,"width":0.015277778,"height":0.024444444},"on_screen":false,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"Replace","depth":2,"on_screen":false,"role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"New Line","depth":1,"bounds":{"left":0.0,"top":0.0,"width":0.015277778,"height":0.024444444},"on_screen":false,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Preserve case","depth":1,"bounds":{"left":0.0,"top":0.0,"width":0.015277778,"height":0.024444444},"on_screen":false,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"In Project","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Module","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Directory","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Scope","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"Module","depth":1,"bounds":{"left":0.0,"top":0.0,"width":0.20833333,"height":0.037777778},"on_screen":false,"role_description":"pop up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXComboBox","text":"/Users/lukas/jiminny/app/app/Services/Crm/Hubspot","depth":1,"bounds":{"left":0.0,"top":0.0,"width":0.4125,"height":0.037777778},"on_screen":false,"value":"/Users/lukas/jiminny/app/app/Services/Crm/Hubspot","role_description":"combo box","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/Hubspot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Exceptions","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/Queue/Job","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/AutomatedReports/UserPilot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Events/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Jobs/AutomatedReports","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/Activities/Coaching/UserPilot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/Activities/ActivityProvider/UserPilot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Jobs/Activity/PushSummaryToCrm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Repositories/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Jobs/Crm/Delete","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Kiosk/AutomatedReports","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Http/Controllers/API/UserAutomatedReports","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/Salesforce","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Providers","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/IntegrationApp","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Events/Activities/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/Playbooks","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Console/Commands/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Http/Controllers","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Console/Commands/Reports","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/VO/Repository/OnDemandActivitySearch","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/Activities/Conferences/UserPilot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Http/Controllers/Webhook","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/resources/views/emails/reports","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Mail/Reports","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Repositories","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/ActivitySearch/Service","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Salesforce","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/routes","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Console/Commands","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/database/migrations","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/Library/Application Support/JetBrains/PhpStorm2026.1/consoles/db/325d461a-c90f-430a-99d4-6ddfce0c61d7","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Http/Controllers/API/V2","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Jobs/Crm/Hubspot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/DealInsights","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Policies","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/Helpers","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Jobs/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Models","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/Teams","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Jobs/Crm/Salesforce","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/Journal","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/ServiceTraits","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/OpportunitySyncStrategy","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/Webhook","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/storage/logs","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Internal","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/Transcription","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/tests/Unit/Listeners/Teams","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Models/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/Library/Application Support/JetBrains/PhpStorm2026.1/consoles/db/91133dfa-8d71-4e12-bfb8-fec7f1afba8f","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Observers","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Mail","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Console/Commands/Activities","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Console/Commands/Activities/Migrator","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot/ServiceTraits","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Jobs/User","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Models/Activity","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot/Webhook","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/AiAutomation/Listeners/PendingAnalysis","depth":6,"on_screen":false,"role_description":"text"}]...
|
39284957202436484
|
-305738695520194621
|
click
|
accessibility
|
NULL
|
Find in Files
97 matches in 21 files
File mask:
*. Find in Files
97 matches in 21 files
File mask:
*.php
*.php
Auto
*.php
Filter Search Results
Pin Window
Search History
makeRequest
New Line
Match case
Words
Regex
Replace History
Replace
New Line
Preserve case
In Project
Module
Directory
Scope
Module
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot
/Users/lukas/jiminny/app/app/Exceptions
/Users/lukas/jiminny/app/app/Listeners/Crm
/Users/lukas/jiminny/app/app/Component/Queue/Job
/Users/lukas/jiminny/app/app/Listeners/AutomatedReports/UserPilot
/Users/lukas/jiminny/app/app/Events/Crm
/Users/lukas/jiminny/app/app/Jobs/AutomatedReports
/Users/lukas/jiminny/app/app/Listeners/Activities/Coaching/UserPilot
/Users/lukas/jiminny/app/app/Listeners/Activities/ActivityProvider/UserPilot
/Users/lukas/jiminny/app/app/Jobs/Activity/PushSummaryToCrm
/Users/lukas/jiminny/app/app/Repositories/Crm
/Users/lukas/jiminny/app/app/Jobs/Crm/Delete
/Users/lukas/jiminny/app/app/Services/Kiosk/AutomatedReports
/Users/lukas/jiminny/app/app/Http/Controllers/API/UserAutomatedReports
/Users/lukas/jiminny/app/app/Services/Crm/Salesforce
/Users/lukas/jiminny/app/app/Providers
/Users/lukas/jiminny/app/app/Services/Crm/IntegrationApp
/Users/lukas/jiminny/app/app/Events/Activities/Crm
/Users/lukas/jiminny/app/app/Listeners/Playbooks
/Users/lukas/jiminny/app/app/Console/Commands/Crm
/Users/lukas/jiminny/app/app/Services/Crm
/Users/lukas/jiminny/app/app/Http/Controllers
/Users/lukas/jiminny/app/app/Console/Commands/Reports
/Users/lukas/jiminny/app/app/VO/Repository/OnDemandActivitySearch
/Users/lukas/jiminny/app/app/Listeners/Activities/Conferences/UserPilot
/Users/lukas/jiminny/app/app/Http/Controllers/Webhook
/Users/lukas/jiminny/app/resources/views/emails/reports
/Users/lukas/jiminny/app/app/Mail/Reports
/Users/lukas/jiminny/app/app/Repositories
/Users/lukas/jiminny/app/app/Component/ActivitySearch/Service
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Salesforce
/Users/lukas/jiminny/app/routes
/Users/lukas/jiminny/app/app/Console/Commands
/Users/lukas/jiminny/app/database/migrations
/Users/lukas/Library/Application Support/JetBrains/PhpStorm2026.1/consoles/db/325d461a-c90f-430a-99d4-6ddfce0c61d7
/Users/lukas/jiminny/app/app/Http/Controllers/API/V2
/Users/lukas/jiminny/app/app/Jobs/Crm/Hubspot
/Users/lukas/jiminny/app/app/Component/DealInsights
/Users/lukas/jiminny/app/app/Policies
/Users/lukas/jiminny/app/app/Services/Crm/Helpers
/Users/lukas/jiminny/app/app/Jobs/Crm
/Users/lukas/jiminny/app/app/Models
/Users/lukas/jiminny/app/app/Listeners/Teams
/Users/lukas/jiminny/app/app/Jobs/Crm/Salesforce
/Users/lukas/jiminny/app/app
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/Journal
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/ServiceTraits
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/OpportunitySyncStrategy
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/Webhook
/Users/lukas/jiminny/app/storage/logs
/Users/lukas/jiminny/app
/Users/lukas/jiminny/app/app/Services/Internal
/Users/lukas/jiminny/app/app/Listeners/Transcription
/Users/lukas/jiminny/app/tests/Unit/Listeners/Teams
/Users/lukas/jiminny/app/app/Models/Crm
/Users/lukas/Library/Application Support/JetBrains/PhpStorm2026.1/consoles/db/91133dfa-8d71-4e12-bfb8-fec7f1afba8f
/Users/lukas/jiminny/app/app/Observers
/Users/lukas/jiminny/app/app/Services/Mail
/Users/lukas/jiminny/app/app/Console/Commands/Activities
/Users/lukas/jiminny/app/app/Console/Commands/Activities/Migrator
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot/ServiceTraits
/Users/lukas/jiminny/app/app/Jobs/User
/Users/lukas/jiminny/app/app/Models/Activity
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot/Webhook
/Users/lukas/jiminny/app/app/Component/AiAutomation/Listeners/PendingAnalysis...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
2859
|
114
|
45
|
2026-05-07T11:43:40.682036+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778154220682_m2.jpg...
|
PhpStorm
|
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Find in Files
97 matches in 21 files
File mask:
*. Find in Files
97 matches in 21 files
File mask:
*.php
*.php
Auto
*.php
Filter Search Results
Pin Window
Search History
makeRequest
New Line
Match case
Words
Regex
Replace History
Replace
New Line
Preserve case
In Project
Module
Directory
Scope
Module
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot
/Users/lukas/jiminny/app/app/Exceptions
/Users/lukas/jiminny/app/app/Listeners/Crm
/Users/lukas/jiminny/app/app/Component/Queue/Job
/Users/lukas/jiminny/app/app/Listeners/AutomatedReports/UserPilot
/Users/lukas/jiminny/app/app/Events/Crm
/Users/lukas/jiminny/app/app/Jobs/AutomatedReports
/Users/lukas/jiminny/app/app/Listeners/Activities/Coaching/UserPilot
/Users/lukas/jiminny/app/app/Listeners/Activities/ActivityProvider/UserPilot
/Users/lukas/jiminny/app/app/Jobs/Activity/PushSummaryToCrm
/Users/lukas/jiminny/app/app/Repositories/Crm
/Users/lukas/jiminny/app/app/Jobs/Crm/Delete
/Users/lukas/jiminny/app/app/Services/Kiosk/AutomatedReports
/Users/lukas/jiminny/app/app/Http/Controllers/API/UserAutomatedReports
/Users/lukas/jiminny/app/app/Services/Crm/Salesforce
/Users/lukas/jiminny/app/app/Providers
/Users/lukas/jiminny/app/app/Services/Crm/IntegrationApp
/Users/lukas/jiminny/app/app/Events/Activities/Crm
/Users/lukas/jiminny/app/app/Listeners/Playbooks
/Users/lukas/jiminny/app/app/Console/Commands/Crm
/Users/lukas/jiminny/app/app/Services/Crm
/Users/lukas/jiminny/app/app/Http/Controllers
/Users/lukas/jiminny/app/app/Console/Commands/Reports
/Users/lukas/jiminny/app/app/VO/Repository/OnDemandActivitySearch
/Users/lukas/jiminny/app/app/Listeners/Activities/Conferences/UserPilot
/Users/lukas/jiminny/app/app/Http/Controllers/Webhook
/Users/lukas/jiminny/app/resources/views/emails/reports
/Users/lukas/jiminny/app/app/Mail/Reports
/Users/lukas/jiminny/app/app/Repositories
/Users/lukas/jiminny/app/app/Component/ActivitySearch/Service
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Salesforce
/Users/lukas/jiminny/app/routes
/Users/lukas/jiminny/app/app/Console/Commands
/Users/lukas/jiminny/app/database/migrations
/Users/lukas/Library/Application Support/JetBrains/PhpStorm2026.1/consoles/db/325d461a-c90f-430a-99d4-6ddfce0c61d7
/Users/lukas/jiminny/app/app/Http/Controllers/API/V2
/Users/lukas/jiminny/app/app/Jobs/Crm/Hubspot
/Users/lukas/jiminny/app/app/Component/DealInsights
/Users/lukas/jiminny/app/app/Policies
/Users/lukas/jiminny/app/app/Services/Crm/Helpers
/Users/lukas/jiminny/app/app/Jobs/Crm
/Users/lukas/jiminny/app/app/Models
/Users/lukas/jiminny/app/app/Listeners/Teams
/Users/lukas/jiminny/app/app/Jobs/Crm/Salesforce
/Users/lukas/jiminny/app/app
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/Journal
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/ServiceTraits
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/OpportunitySyncStrategy
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/Webhook
/Users/lukas/jiminny/app/storage/logs
/Users/lukas/jiminny/app
/Users/lukas/jiminny/app/app/Services/Internal
/Users/lukas/jiminny/app/app/Listeners/Transcription
/Users/lukas/jiminny/app/tests/Unit/Listeners/Teams
/Users/lukas/jiminny/app/app/Models/Crm
/Users/lukas/Library/Application Support/JetBrains/PhpStorm2026.1/consoles/db/91133dfa-8d71-4e12-bfb8-fec7f1afba8f
/Users/lukas/jiminny/app/app/Observers
/Users/lukas/jiminny/app/app/Services/Mail
/Users/lukas/jiminny/app/app/Console/Commands/Activities
/Users/lukas/jiminny/app/app/Console/Commands/Activities/Migrator
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot/ServiceTraits
/Users/lukas/jiminny/app/app/Jobs/User
/Users/lukas/jiminny/app/app/Models/Activity
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot/Webhook...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"Find in Files","depth":1,"bounds":{"left":0.2992021,"top":0.12609737,"width":0.024601065,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"97 matches in 21 files","depth":1,"bounds":{"left":0.32779256,"top":0.12609737,"width":0.044215426,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"File mask:","depth":1,"bounds":{"left":0.5315825,"top":0.12290503,"width":0.029587766,"height":0.019952115},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXComboBox","text":"*.php","depth":1,"bounds":{"left":0.5621675,"top":0.11971269,"width":0.027925532,"height":0.027134877},"on_screen":true,"value":"*.php","role_description":"combo box","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"*.php","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Auto","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXTextField","text":"*.php","depth":2,"bounds":{"left":0.5661569,"top":0.12609737,"width":0.011635638,"height":0.013567438},"on_screen":true,"value":"*.php","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Filter Search Results","depth":1,"bounds":{"left":0.5944149,"top":0.12290503,"width":0.00731383,"height":0.017557861},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Pin Window","depth":1,"bounds":{"left":0.6037234,"top":0.12290503,"width":0.00731383,"height":0.017557861},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Search History","depth":1,"bounds":{"left":0.2962101,"top":0.15403032,"width":0.00731383,"height":0.017557861},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"makeRequest","depth":2,"bounds":{"left":0.30718085,"top":0.15403032,"width":0.26196808,"height":0.017557861},"on_screen":true,"value":"makeRequest","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"New Line","depth":1,"bounds":{"left":0.578125,"top":0.15403032,"width":0.00731383,"height":0.017557861},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Match case","depth":1,"bounds":{"left":0.5880984,"top":0.15403032,"width":0.00731383,"height":0.017557861},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Words","depth":1,"bounds":{"left":0.59674203,"top":0.15403032,"width":0.00731383,"height":0.017557861},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Regex","depth":1,"bounds":{"left":0.60538566,"top":0.15403032,"width":0.00731383,"height":0.017557861},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Replace History","depth":1,"bounds":{"left":0.27027926,"top":1.0,"width":0.00731383,"height":0.0},"on_screen":false,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"Replace","depth":2,"on_screen":false,"role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"New Line","depth":1,"bounds":{"left":0.27027926,"top":1.0,"width":0.00731383,"height":0.0},"on_screen":false,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Preserve case","depth":1,"bounds":{"left":0.27027926,"top":1.0,"width":0.00731383,"height":0.0},"on_screen":false,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"In Project","depth":2,"bounds":{"left":0.2992021,"top":0.1867518,"width":0.022938829,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Module","depth":2,"bounds":{"left":0.32214096,"top":0.1867518,"width":0.019281914,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Directory","depth":2,"bounds":{"left":0.3414229,"top":0.1867518,"width":0.022606382,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Scope","depth":2,"bounds":{"left":0.36402926,"top":0.1867518,"width":0.017287234,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"Module","depth":1,"bounds":{"left":0.27027926,"top":1.0,"width":0.099734046,"height":0.0},"on_screen":false,"role_description":"pop up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXComboBox","text":"/Users/lukas/jiminny/app/app/Services/Crm/Hubspot","depth":1,"bounds":{"left":0.27027926,"top":1.0,"width":0.1974734,"height":0.0},"on_screen":false,"value":"/Users/lukas/jiminny/app/app/Services/Crm/Hubspot","role_description":"combo box","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/Hubspot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Exceptions","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/Queue/Job","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/AutomatedReports/UserPilot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Events/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Jobs/AutomatedReports","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/Activities/Coaching/UserPilot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/Activities/ActivityProvider/UserPilot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Jobs/Activity/PushSummaryToCrm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Repositories/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Jobs/Crm/Delete","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Kiosk/AutomatedReports","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Http/Controllers/API/UserAutomatedReports","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/Salesforce","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Providers","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/IntegrationApp","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Events/Activities/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/Playbooks","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Console/Commands/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Http/Controllers","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Console/Commands/Reports","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/VO/Repository/OnDemandActivitySearch","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/Activities/Conferences/UserPilot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Http/Controllers/Webhook","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/resources/views/emails/reports","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Mail/Reports","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Repositories","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/ActivitySearch/Service","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Salesforce","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/routes","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Console/Commands","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/database/migrations","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/Library/Application Support/JetBrains/PhpStorm2026.1/consoles/db/325d461a-c90f-430a-99d4-6ddfce0c61d7","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Http/Controllers/API/V2","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Jobs/Crm/Hubspot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/DealInsights","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Policies","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/Helpers","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Jobs/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Models","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/Teams","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Jobs/Crm/Salesforce","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/Journal","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/ServiceTraits","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/OpportunitySyncStrategy","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/Webhook","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/storage/logs","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Internal","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/Transcription","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/tests/Unit/Listeners/Teams","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Models/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/Library/Application Support/JetBrains/PhpStorm2026.1/consoles/db/91133dfa-8d71-4e12-bfb8-fec7f1afba8f","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Observers","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Mail","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Console/Commands/Activities","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Console/Commands/Activities/Migrator","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot/ServiceTraits","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Jobs/User","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Models/Activity","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot/Webhook","depth":6,"on_screen":false,"role_description":"text"}]...
|
3381457612748173815
|
-305888231251155005
|
click
|
accessibility
|
NULL
|
Find in Files
97 matches in 21 files
File mask:
*. Find in Files
97 matches in 21 files
File mask:
*.php
*.php
Auto
*.php
Filter Search Results
Pin Window
Search History
makeRequest
New Line
Match case
Words
Regex
Replace History
Replace
New Line
Preserve case
In Project
Module
Directory
Scope
Module
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot
/Users/lukas/jiminny/app/app/Exceptions
/Users/lukas/jiminny/app/app/Listeners/Crm
/Users/lukas/jiminny/app/app/Component/Queue/Job
/Users/lukas/jiminny/app/app/Listeners/AutomatedReports/UserPilot
/Users/lukas/jiminny/app/app/Events/Crm
/Users/lukas/jiminny/app/app/Jobs/AutomatedReports
/Users/lukas/jiminny/app/app/Listeners/Activities/Coaching/UserPilot
/Users/lukas/jiminny/app/app/Listeners/Activities/ActivityProvider/UserPilot
/Users/lukas/jiminny/app/app/Jobs/Activity/PushSummaryToCrm
/Users/lukas/jiminny/app/app/Repositories/Crm
/Users/lukas/jiminny/app/app/Jobs/Crm/Delete
/Users/lukas/jiminny/app/app/Services/Kiosk/AutomatedReports
/Users/lukas/jiminny/app/app/Http/Controllers/API/UserAutomatedReports
/Users/lukas/jiminny/app/app/Services/Crm/Salesforce
/Users/lukas/jiminny/app/app/Providers
/Users/lukas/jiminny/app/app/Services/Crm/IntegrationApp
/Users/lukas/jiminny/app/app/Events/Activities/Crm
/Users/lukas/jiminny/app/app/Listeners/Playbooks
/Users/lukas/jiminny/app/app/Console/Commands/Crm
/Users/lukas/jiminny/app/app/Services/Crm
/Users/lukas/jiminny/app/app/Http/Controllers
/Users/lukas/jiminny/app/app/Console/Commands/Reports
/Users/lukas/jiminny/app/app/VO/Repository/OnDemandActivitySearch
/Users/lukas/jiminny/app/app/Listeners/Activities/Conferences/UserPilot
/Users/lukas/jiminny/app/app/Http/Controllers/Webhook
/Users/lukas/jiminny/app/resources/views/emails/reports
/Users/lukas/jiminny/app/app/Mail/Reports
/Users/lukas/jiminny/app/app/Repositories
/Users/lukas/jiminny/app/app/Component/ActivitySearch/Service
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Salesforce
/Users/lukas/jiminny/app/routes
/Users/lukas/jiminny/app/app/Console/Commands
/Users/lukas/jiminny/app/database/migrations
/Users/lukas/Library/Application Support/JetBrains/PhpStorm2026.1/consoles/db/325d461a-c90f-430a-99d4-6ddfce0c61d7
/Users/lukas/jiminny/app/app/Http/Controllers/API/V2
/Users/lukas/jiminny/app/app/Jobs/Crm/Hubspot
/Users/lukas/jiminny/app/app/Component/DealInsights
/Users/lukas/jiminny/app/app/Policies
/Users/lukas/jiminny/app/app/Services/Crm/Helpers
/Users/lukas/jiminny/app/app/Jobs/Crm
/Users/lukas/jiminny/app/app/Models
/Users/lukas/jiminny/app/app/Listeners/Teams
/Users/lukas/jiminny/app/app/Jobs/Crm/Salesforce
/Users/lukas/jiminny/app/app
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/Journal
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/ServiceTraits
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/OpportunitySyncStrategy
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/Webhook
/Users/lukas/jiminny/app/storage/logs
/Users/lukas/jiminny/app
/Users/lukas/jiminny/app/app/Services/Internal
/Users/lukas/jiminny/app/app/Listeners/Transcription
/Users/lukas/jiminny/app/tests/Unit/Listeners/Teams
/Users/lukas/jiminny/app/app/Models/Crm
/Users/lukas/Library/Application Support/JetBrains/PhpStorm2026.1/consoles/db/91133dfa-8d71-4e12-bfb8-fec7f1afba8f
/Users/lukas/jiminny/app/app/Observers
/Users/lukas/jiminny/app/app/Services/Mail
/Users/lukas/jiminny/app/app/Console/Commands/Activities
/Users/lukas/jiminny/app/app/Console/Commands/Activities/Migrator
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot/ServiceTraits
/Users/lukas/jiminny/app/app/Jobs/User
/Users/lukas/jiminny/app/app/Models/Activity
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot/Webhook...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
2858
|
113
|
45
|
2026-05-07T11:43:40.682045+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778154220682_m1.jpg...
|
PhpStorm
|
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Find in Files
97 matches in 21 files
File mask:
*. Find in Files
97 matches in 21 files
File mask:
*.php
*.php
Auto
*.php
Filter Search Results
Pin Window
Search History
makeRequest
New Line
Match case
Words
Regex
Replace History
Replace
New Line
Preserve case
In Project
Module
Directory
Scope
Module
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot
/Users/lukas/jiminny/app/app/Exceptions
/Users/lukas/jiminny/app/app/Listeners/Crm
/Users/lukas/jiminny/app/app/Component/Queue/Job
/Users/lukas/jiminny/app/app/Listeners/AutomatedReports/UserPilot
/Users/lukas/jiminny/app/app/Events/Crm
/Users/lukas/jiminny/app/app/Jobs/AutomatedReports
/Users/lukas/jiminny/app/app/Listeners/Activities/Coaching/UserPilot
/Users/lukas/jiminny/app/app/Listeners/Activities/ActivityProvider/UserPilot
/Users/lukas/jiminny/app/app/Jobs/Activity/PushSummaryToCrm
/Users/lukas/jiminny/app/app/Repositories/Crm
/Users/lukas/jiminny/app/app/Jobs/Crm/Delete
/Users/lukas/jiminny/app/app/Services/Kiosk/AutomatedReports
/Users/lukas/jiminny/app/app/Http/Controllers/API/UserAutomatedReports
/Users/lukas/jiminny/app/app/Services/Crm/Salesforce
/Users/lukas/jiminny/app/app/Providers
/Users/lukas/jiminny/app/app/Services/Crm/IntegrationApp
/Users/lukas/jiminny/app/app/Events/Activities/Crm
/Users/lukas/jiminny/app/app/Listeners/Playbooks
/Users/lukas/jiminny/app/app/Console/Commands/Crm
/Users/lukas/jiminny/app/app/Services/Crm
/Users/lukas/jiminny/app/app/Http/Controllers
/Users/lukas/jiminny/app/app/Console/Commands/Reports
/Users/lukas/jiminny/app/app/VO/Repository/OnDemandActivitySearch
/Users/lukas/jiminny/app/app/Listeners/Activities/Conferences/UserPilot
/Users/lukas/jiminny/app/app/Http/Controllers/Webhook
/Users/lukas/jiminny/app/resources/views/emails/reports
/Users/lukas/jiminny/app/app/Mail/Reports
/Users/lukas/jiminny/app/app/Repositories
/Users/lukas/jiminny/app/app/Component/ActivitySearch/Service
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Salesforce
/Users/lukas/jiminny/app/routes
/Users/lukas/jiminny/app/app/Console/Commands
/Users/lukas/jiminny/app/database/migrations
/Users/lukas/Library/Application Support/JetBrains/PhpStorm2026.1/consoles/db/325d461a-c90f-430a-99d4-6ddfce0c61d7
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot
Select Directory
Scope
Edit Scopes
Usages
$response = $client->makeRequest($endpoint, 'GET', [], $queryString); in FetchMergedObjectsPageJob.php 341
public function canMakeRequest(RateLimited $provider): bool in ProviderRateLimiter.php 20
private function makeRequest(int &$page): \GuzzleHttp\Promise\PromiseInterface|\Illuminate\Http\Client\Response in MigrateFromLeexiCommand.php 30
$response = $this->makeRequest($page); in MigrateFromLeexiCommand.php 51
if ($this->rateLimiter->canMakeRequest($this->service->getProvider())) { in Import/DownloadTrack.php 94
if (! $rateLimiter->canMakeRequest($activity->getCrm())) { in MatchCrmData.php 103
protected function makeRequest(array $params): iterable in BullhornLastModifiedSyncStrategy.php 20
protected function makeRequest(array $params): iterable in BullhornSingleSyncStrategy.php 18
return $this->makeRequest($params); in BullhornSyncStrategyBase.php 27
abstract protected function makeRequest(array $params): iterable; in BullhornSyncStrategyBase.php 56
if (! $this->rateLimiter->canMakeRequest($this->config)) { in Crm/Hubspot/Client.php 83
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals'); in Crm/Hubspot/Client.php 583
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null) in Crm/Hubspot/Client.php 701
return $this->makeRequest($endpoint, 'POST', $payload); in Crm/Hubspot/Client.php 738...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"Find in Files","depth":1,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"97 matches in 21 files","depth":1,"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"File mask:","depth":1,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXComboBox","text":"*.php","depth":1,"on_screen":true,"value":"*.php","role_description":"combo box","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"*.php","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Auto","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXTextField","text":"*.php","depth":2,"on_screen":true,"value":"*.php","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Filter Search Results","depth":1,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Pin Window","depth":1,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Search History","depth":1,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"makeRequest","depth":2,"on_screen":true,"value":"makeRequest","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"New Line","depth":1,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Match case","depth":1,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Words","depth":1,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Regex","depth":1,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Replace History","depth":1,"bounds":{"left":0.0,"top":0.0,"width":0.015277778,"height":0.024444444},"on_screen":false,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"Replace","depth":2,"on_screen":false,"role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"New Line","depth":1,"bounds":{"left":0.0,"top":0.0,"width":0.015277778,"height":0.024444444},"on_screen":false,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Preserve case","depth":1,"bounds":{"left":0.0,"top":0.0,"width":0.015277778,"height":0.024444444},"on_screen":false,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"In Project","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Module","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Directory","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Scope","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"Module","depth":1,"bounds":{"left":0.0,"top":0.0,"width":0.20833333,"height":0.037777778},"on_screen":false,"role_description":"pop up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXComboBox","text":"/Users/lukas/jiminny/app/app/Services/Crm/Hubspot","depth":1,"bounds":{"left":0.0,"top":0.0,"width":0.4125,"height":0.037777778},"on_screen":false,"value":"/Users/lukas/jiminny/app/app/Services/Crm/Hubspot","role_description":"combo box","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/Hubspot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Exceptions","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/Queue/Job","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/AutomatedReports/UserPilot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Events/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Jobs/AutomatedReports","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/Activities/Coaching/UserPilot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/Activities/ActivityProvider/UserPilot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Jobs/Activity/PushSummaryToCrm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Repositories/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Jobs/Crm/Delete","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Kiosk/AutomatedReports","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Http/Controllers/API/UserAutomatedReports","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/Salesforce","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Providers","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/IntegrationApp","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Events/Activities/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/Playbooks","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Console/Commands/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Http/Controllers","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Console/Commands/Reports","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/VO/Repository/OnDemandActivitySearch","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/Activities/Conferences/UserPilot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Http/Controllers/Webhook","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/resources/views/emails/reports","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Mail/Reports","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Repositories","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/ActivitySearch/Service","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Salesforce","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/routes","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Console/Commands","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/database/migrations","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/Library/Application Support/JetBrains/PhpStorm2026.1/consoles/db/325d461a-c90f-430a-99d4-6ddfce0c61d7","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXTextField","text":"/Users/lukas/jiminny/app/app/Services/Crm/Hubspot","depth":2,"bounds":{"left":0.0,"top":0.0,"width":0.3784722,"height":0.018888889},"on_screen":false,"value":"/Users/lukas/jiminny/app/app/Services/Crm/Hubspot","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Select Directory","depth":1,"bounds":{"left":0.0,"top":0.0,"width":0.023611112,"height":0.037777778},"on_screen":false,"help_text":"⇧⏎","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"Scope","depth":1,"bounds":{"left":0.0,"top":0.0,"width":0.20833333,"height":0.037777778},"on_screen":false,"role_description":"pop up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Edit Scopes","depth":1,"bounds":{"left":0.0,"top":0.0,"width":0.023611112,"height":0.037777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Usages","depth":2,"on_screen":false,"role_description":"text"},{"role":"AXCell","text":"$response = $client->makeRequest($endpoint, 'GET', [], $queryString); in FetchMergedObjectsPageJob.php 341","depth":4,"on_screen":true,"role_description":"cell"},{"role":"AXCell","text":"public function canMakeRequest(RateLimited $provider): bool in ProviderRateLimiter.php 20","depth":4,"on_screen":true,"role_description":"cell"},{"role":"AXCell","text":"private function makeRequest(int &$page): \\GuzzleHttp\\Promise\\PromiseInterface|\\Illuminate\\Http\\Client\\Response in MigrateFromLeexiCommand.php 30","depth":4,"on_screen":true,"role_description":"cell"},{"role":"AXCell","text":"$response = $this->makeRequest($page); in MigrateFromLeexiCommand.php 51","depth":4,"on_screen":true,"role_description":"cell"},{"role":"AXCell","text":"if ($this->rateLimiter->canMakeRequest($this->service->getProvider())) { in Import/DownloadTrack.php 94","depth":4,"on_screen":true,"role_description":"cell"},{"role":"AXCell","text":"if (! $rateLimiter->canMakeRequest($activity->getCrm())) { in MatchCrmData.php 103","depth":4,"on_screen":true,"role_description":"cell"},{"role":"AXCell","text":"protected function makeRequest(array $params): iterable in BullhornLastModifiedSyncStrategy.php 20","depth":4,"on_screen":true,"role_description":"cell"},{"role":"AXCell","text":"protected function makeRequest(array $params): iterable in BullhornSingleSyncStrategy.php 18","depth":4,"on_screen":true,"role_description":"cell"},{"role":"AXCell","text":"return $this->makeRequest($params); in BullhornSyncStrategyBase.php 27","depth":4,"on_screen":true,"role_description":"cell"},{"role":"AXCell","text":"abstract protected function makeRequest(array $params): iterable; in BullhornSyncStrategyBase.php 56","depth":4,"on_screen":true,"role_description":"cell"},{"role":"AXCell","text":"if (! $this->rateLimiter->canMakeRequest($this->config)) { in Crm/Hubspot/Client.php 83","depth":4,"on_screen":true,"role_description":"cell"},{"role":"AXCell","text":"$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals'); in Crm/Hubspot/Client.php 583","depth":4,"on_screen":true,"role_description":"cell"},{"role":"AXCell","text":"public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null) in Crm/Hubspot/Client.php 701","depth":4,"on_screen":true,"role_description":"cell"},{"role":"AXCell","text":"return $this->makeRequest($endpoint, 'POST', $payload); in Crm/Hubspot/Client.php 738","depth":4,"on_screen":true,"role_description":"cell"}]...
|
1858394876802771150
|
4132272572138331523
|
click
|
accessibility
|
NULL
|
Find in Files
97 matches in 21 files
File mask:
*. Find in Files
97 matches in 21 files
File mask:
*.php
*.php
Auto
*.php
Filter Search Results
Pin Window
Search History
makeRequest
New Line
Match case
Words
Regex
Replace History
Replace
New Line
Preserve case
In Project
Module
Directory
Scope
Module
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot
/Users/lukas/jiminny/app/app/Exceptions
/Users/lukas/jiminny/app/app/Listeners/Crm
/Users/lukas/jiminny/app/app/Component/Queue/Job
/Users/lukas/jiminny/app/app/Listeners/AutomatedReports/UserPilot
/Users/lukas/jiminny/app/app/Events/Crm
/Users/lukas/jiminny/app/app/Jobs/AutomatedReports
/Users/lukas/jiminny/app/app/Listeners/Activities/Coaching/UserPilot
/Users/lukas/jiminny/app/app/Listeners/Activities/ActivityProvider/UserPilot
/Users/lukas/jiminny/app/app/Jobs/Activity/PushSummaryToCrm
/Users/lukas/jiminny/app/app/Repositories/Crm
/Users/lukas/jiminny/app/app/Jobs/Crm/Delete
/Users/lukas/jiminny/app/app/Services/Kiosk/AutomatedReports
/Users/lukas/jiminny/app/app/Http/Controllers/API/UserAutomatedReports
/Users/lukas/jiminny/app/app/Services/Crm/Salesforce
/Users/lukas/jiminny/app/app/Providers
/Users/lukas/jiminny/app/app/Services/Crm/IntegrationApp
/Users/lukas/jiminny/app/app/Events/Activities/Crm
/Users/lukas/jiminny/app/app/Listeners/Playbooks
/Users/lukas/jiminny/app/app/Console/Commands/Crm
/Users/lukas/jiminny/app/app/Services/Crm
/Users/lukas/jiminny/app/app/Http/Controllers
/Users/lukas/jiminny/app/app/Console/Commands/Reports
/Users/lukas/jiminny/app/app/VO/Repository/OnDemandActivitySearch
/Users/lukas/jiminny/app/app/Listeners/Activities/Conferences/UserPilot
/Users/lukas/jiminny/app/app/Http/Controllers/Webhook
/Users/lukas/jiminny/app/resources/views/emails/reports
/Users/lukas/jiminny/app/app/Mail/Reports
/Users/lukas/jiminny/app/app/Repositories
/Users/lukas/jiminny/app/app/Component/ActivitySearch/Service
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Salesforce
/Users/lukas/jiminny/app/routes
/Users/lukas/jiminny/app/app/Console/Commands
/Users/lukas/jiminny/app/database/migrations
/Users/lukas/Library/Application Support/JetBrains/PhpStorm2026.1/consoles/db/325d461a-c90f-430a-99d4-6ddfce0c61d7
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot
Select Directory
Scope
Edit Scopes
Usages
$response = $client->makeRequest($endpoint, 'GET', [], $queryString); in FetchMergedObjectsPageJob.php 341
public function canMakeRequest(RateLimited $provider): bool in ProviderRateLimiter.php 20
private function makeRequest(int &$page): \GuzzleHttp\Promise\PromiseInterface|\Illuminate\Http\Client\Response in MigrateFromLeexiCommand.php 30
$response = $this->makeRequest($page); in MigrateFromLeexiCommand.php 51
if ($this->rateLimiter->canMakeRequest($this->service->getProvider())) { in Import/DownloadTrack.php 94
if (! $rateLimiter->canMakeRequest($activity->getCrm())) { in MatchCrmData.php 103
protected function makeRequest(array $params): iterable in BullhornLastModifiedSyncStrategy.php 20
protected function makeRequest(array $params): iterable in BullhornSingleSyncStrategy.php 18
return $this->makeRequest($params); in BullhornSyncStrategyBase.php 27
abstract protected function makeRequest(array $params): iterable; in BullhornSyncStrategyBase.php 56
if (! $this->rateLimiter->canMakeRequest($this->config)) { in Crm/Hubspot/Client.php 83
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals'); in Crm/Hubspot/Client.php 583
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null) in Crm/Hubspot/Client.php 701
return $this->makeRequest($endpoint, 'POST', $payload); in Crm/Hubspot/Client.php 738...
|
2856
|
NULL
|
NULL
|
NULL
|
|
2857
|
114
|
44
|
2026-05-07T11:43:39.468020+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778154219468_m2.jpg...
|
PhpStorm
|
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Find in Files
97 matches in 21 files
File mask:
*. Find in Files
97 matches in 21 files
File mask:
*.php
*.php
Auto
*.php
Filter Search Results
Pin Window
Search History
makeRequest
New Line
Match case
Words
Regex
Replace History
Replace
New Line
Preserve case
In Project
Module
Directory
Scope
Module
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot
/Users/lukas/jiminny/app/app/Exceptions
/Users/lukas/jiminny/app/app/Listeners/Crm
/Users/lukas/jiminny/app/app/Component/Queue/Job
/Users/lukas/jiminny/app/app/Listeners/AutomatedReports/UserPilot
/Users/lukas/jiminny/app/app/Events/Crm
/Users/lukas/jiminny/app/app/Jobs/AutomatedReports
/Users/lukas/jiminny/app/app/Listeners/Activities/Coaching/UserPilot
/Users/lukas/jiminny/app/app/Listeners/Activities/ActivityProvider/UserPilot
/Users/lukas/jiminny/app/app/Jobs/Activity/PushSummaryToCrm
/Users/lukas/jiminny/app/app/Repositories/Crm
/Users/lukas/jiminny/app/app/Jobs/Crm/Delete
/Users/lukas/jiminny/app/app/Services/Kiosk/AutomatedReports
/Users/lukas/jiminny/app/app/Http/Controllers/API/UserAutomatedReports
/Users/lukas/jiminny/app/app/Services/Crm/Salesforce
/Users/lukas/jiminny/app/app/Providers
/Users/lukas/jiminny/app/app/Services/Crm/IntegrationApp
/Users/lukas/jiminny/app/app/Events/Activities/Crm
/Users/lukas/jiminny/app/app/Listeners/Playbooks
/Users/lukas/jiminny/app/app/Console/Commands/Crm
/Users/lukas/jiminny/app/app/Services/Crm
/Users/lukas/jiminny/app/app/Http/Controllers
/Users/lukas/jiminny/app/app/Console/Commands/Reports
/Users/lukas/jiminny/app/app/VO/Repository/OnDemandActivitySearch
/Users/lukas/jiminny/app/app/Listeners/Activities/Conferences/UserPilot
/Users/lukas/jiminny/app/app/Http/Controllers/Webhook
/Users/lukas/jiminny/app/resources/views/emails/reports
/Users/lukas/jiminny/app/app/Mail/Reports
/Users/lukas/jiminny/app/app/Repositories
/Users/lukas/jiminny/app/app/Component/ActivitySearch/Service
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Salesforce
/Users/lukas/jiminny/app/routes
/Users/lukas/jiminny/app/app/Console/Commands
/Users/lukas/jiminny/app/database/migrations
/Users/lukas/Library/Application Support/JetBrains/PhpStorm2026.1/consoles/db/325d461a-c90f-430a-99d4-6ddfce0c61d7
/Users/lukas/jiminny/app/app/Http/Controllers/API/V2
/Users/lukas/jiminny/app/app/Jobs/Crm/Hubspot
/Users/lukas/jiminny/app/app/Component/DealInsights
/Users/lukas/jiminny/app/app/Policies
/Users/lukas/jiminny/app/app/Services/Crm/Helpers
/Users/lukas/jiminny/app/app/Jobs/Crm
/Users/lukas/jiminny/app/app/Models
/Users/lukas/jiminny/app/app/Listeners/Teams
/Users/lukas/jiminny/app/app/Jobs/Crm/Salesforce
/Users/lukas/jiminny/app/app
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/Journal
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/ServiceTraits
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/OpportunitySyncStrategy
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/Webhook
/Users/lukas/jiminny/app/storage/logs
/Users/lukas/jiminny/app
/Users/lukas/jiminny/app/app/Services/Internal
/Users/lukas/jiminny/app/app/Listeners/Transcription
/Users/lukas/jiminny/app/tests/Unit/Listeners/Teams
/Users/lukas/jiminny/app/app/Models/Crm
/Users/lukas/Library/Application Support/JetBrains/PhpStorm2026.1/consoles/db/91133dfa-8d71-4e12-bfb8-fec7f1afba8f
/Users/lukas/jiminny/app/app/Observers
/Users/lukas/jiminny/app/app/Services/Mail
/Users/lukas/jiminny/app/app/Console/Commands/Activities
/Users/lukas/jiminny/app/app/Console/Commands/Activities/Migrator
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot/ServiceTraits
/Users/lukas/jiminny/app/app/Jobs/User
/Users/lukas/jiminny/app/app/Models/Activity
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot/Webhook
/Users/lukas/jiminny/app/app/Component/AiAutomation/Listeners/PendingAnalysis
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot
/Users/lukas/jiminny/app/app/Component/ActivitySearch/FilterDefinition/DealInsights
/Users/lukas/jiminny/app/app/Services/Crm/DecorateActivity
/Users/lukas/jiminny/app/app/Component/Activity/Event
/Users/lukas/jiminny/app/app/Component/Sidekick
/Users/lukas/jiminny/app/app/Listeners/Activities/Conferences
/Users/lukas/jiminny/app/app/Listeners/Activities/Bots
/Users/lukas/jiminny/app/app/Services/RecallAI/Webhooks/Handlers
/Users/lukas/jiminny/app/app/Events/Activities/Bots
/Users/lukas/jiminny/app/app/Component/MeetingBot
/Users/lukas/jiminny/app/app/Services/Activity/RingCentral
/Users/lukas/jiminny/app/app/Http/Controllers/Webhook/Hubspot
/Users/lukas/jiminny/app/app/Services/Activity/Gmail
/Users/lukas/jiminny/app/app/Services/Crm/CrmObjects/ServiceTraits
/Users/lukas/jiminny/app/app/Jobs/Mailbox
/Users/lukas/jiminny/app/app/Console
/Users/lukas/jiminny/app/front-end/src/composables
/Users/lukas/jiminny/app/app/Console/Commands/Calendars
/Users/lukas/jiminny/app/app/Http/Controllers/API
/Users/lukas/jiminny/app/app/Http/Controllers/Internal/WebhookReceiver
/Users/lukas/jiminny/app/app/Services/Crm/IntegrationApp/ServiceTraits
/Users/lukas/jiminny/app/app/Component/Queue
/Users/lukas/jiminny/app/app/Console/Commands/Crm/Hubspot
/Users/lukas/jiminny/app/app/Component/Transcription/Job
/Users/lukas/jiminny/app/tests/Unit/Services/Listeners
/Users/lukas/jiminny/app/app/Services/Crm/Listeners
/Users/lukas/jiminny/app/app/Traits
/Users/lukas/jiminny/app/tests/Unit/Jobs/Crm/Hubspot
/Users/lukas/jiminny/app/tests/Unit/Services/Crm
/Users/lukas/jiminny/app/app/Services/Activity
/Users/lukas/jiminny/app/app/Services/Calendar/Command
/Users/lukas/jiminny/app/.idea/queries
/Users/lukas/jiminny/app/vendor/hubspot/api-client/codegen/Crm
/Users/lukas/jiminny/app/vendor/hubspot
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/Fields
/Users/lukas/jiminny/app/app/Services/Crm/Copper
/Users/lukas/jiminny/app/app/Services/Crm/Bullhorn
/Users/lukas/jiminny/app/app/Notifications/Channels
/Users/lukas/jiminny/app/tests/Unit
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot/Journal
/Users/lukas/jiminny/app/app/Interactions/Settings/Teams
/Users/lukas/jiminny/app/app/Exceptions/Crm...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"Find in Files","depth":1,"bounds":{"left":0.2992021,"top":0.12609737,"width":0.024601065,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"97 matches in 21 files","depth":1,"bounds":{"left":0.32779256,"top":0.12609737,"width":0.044215426,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"File mask:","depth":1,"bounds":{"left":0.5315825,"top":0.12290503,"width":0.029587766,"height":0.019952115},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXComboBox","text":"*.php","depth":1,"bounds":{"left":0.5621675,"top":0.11971269,"width":0.027925532,"height":0.027134877},"on_screen":true,"value":"*.php","role_description":"combo box","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"*.php","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Auto","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXTextField","text":"*.php","depth":2,"bounds":{"left":0.5661569,"top":0.12609737,"width":0.011635638,"height":0.013567438},"on_screen":true,"value":"*.php","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Filter Search Results","depth":1,"bounds":{"left":0.5944149,"top":0.12290503,"width":0.00731383,"height":0.017557861},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Pin Window","depth":1,"bounds":{"left":0.6037234,"top":0.12290503,"width":0.00731383,"height":0.017557861},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Search History","depth":1,"bounds":{"left":0.2962101,"top":0.15403032,"width":0.00731383,"height":0.017557861},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"makeRequest","depth":2,"bounds":{"left":0.30718085,"top":0.15403032,"width":0.26196808,"height":0.017557861},"on_screen":true,"value":"makeRequest","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"New Line","depth":1,"bounds":{"left":0.578125,"top":0.15403032,"width":0.00731383,"height":0.017557861},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Match case","depth":1,"bounds":{"left":0.5880984,"top":0.15403032,"width":0.00731383,"height":0.017557861},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Words","depth":1,"bounds":{"left":0.59674203,"top":0.15403032,"width":0.00731383,"height":0.017557861},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Regex","depth":1,"bounds":{"left":0.60538566,"top":0.15403032,"width":0.00731383,"height":0.017557861},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Replace History","depth":1,"bounds":{"left":0.27027926,"top":1.0,"width":0.00731383,"height":0.0},"on_screen":false,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"Replace","depth":2,"on_screen":false,"role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"New Line","depth":1,"bounds":{"left":0.27027926,"top":1.0,"width":0.00731383,"height":0.0},"on_screen":false,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Preserve case","depth":1,"bounds":{"left":0.27027926,"top":1.0,"width":0.00731383,"height":0.0},"on_screen":false,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"In Project","depth":2,"bounds":{"left":0.2992021,"top":0.1867518,"width":0.022938829,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Module","depth":2,"bounds":{"left":0.32214096,"top":0.1867518,"width":0.019281914,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Directory","depth":2,"bounds":{"left":0.3414229,"top":0.1867518,"width":0.022606382,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Scope","depth":2,"bounds":{"left":0.36402926,"top":0.1867518,"width":0.017287234,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"Module","depth":1,"bounds":{"left":0.27027926,"top":1.0,"width":0.099734046,"height":0.0},"on_screen":false,"role_description":"pop up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXComboBox","text":"/Users/lukas/jiminny/app/app/Services/Crm/Hubspot","depth":1,"bounds":{"left":0.27027926,"top":1.0,"width":0.1974734,"height":0.0},"on_screen":false,"value":"/Users/lukas/jiminny/app/app/Services/Crm/Hubspot","role_description":"combo box","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/Hubspot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Exceptions","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/Queue/Job","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/AutomatedReports/UserPilot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Events/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Jobs/AutomatedReports","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/Activities/Coaching/UserPilot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/Activities/ActivityProvider/UserPilot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Jobs/Activity/PushSummaryToCrm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Repositories/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Jobs/Crm/Delete","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Kiosk/AutomatedReports","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Http/Controllers/API/UserAutomatedReports","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/Salesforce","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Providers","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/IntegrationApp","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Events/Activities/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/Playbooks","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Console/Commands/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Http/Controllers","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Console/Commands/Reports","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/VO/Repository/OnDemandActivitySearch","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/Activities/Conferences/UserPilot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Http/Controllers/Webhook","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/resources/views/emails/reports","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Mail/Reports","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Repositories","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/ActivitySearch/Service","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Salesforce","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/routes","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Console/Commands","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/database/migrations","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/Library/Application Support/JetBrains/PhpStorm2026.1/consoles/db/325d461a-c90f-430a-99d4-6ddfce0c61d7","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Http/Controllers/API/V2","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Jobs/Crm/Hubspot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/DealInsights","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Policies","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/Helpers","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Jobs/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Models","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/Teams","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Jobs/Crm/Salesforce","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/Journal","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/ServiceTraits","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/OpportunitySyncStrategy","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/Webhook","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/storage/logs","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Internal","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/Transcription","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/tests/Unit/Listeners/Teams","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Models/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/Library/Application Support/JetBrains/PhpStorm2026.1/consoles/db/91133dfa-8d71-4e12-bfb8-fec7f1afba8f","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Observers","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Mail","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Console/Commands/Activities","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Console/Commands/Activities/Migrator","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot/ServiceTraits","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Jobs/User","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Models/Activity","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot/Webhook","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/AiAutomation/Listeners/PendingAnalysis","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/ActivitySearch/FilterDefinition/DealInsights","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/DecorateActivity","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/Activity/Event","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/Sidekick","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/Activities/Conferences","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/Activities/Bots","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/RecallAI/Webhooks/Handlers","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Events/Activities/Bots","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/MeetingBot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Activity/RingCentral","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Http/Controllers/Webhook/Hubspot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Activity/Gmail","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/CrmObjects/ServiceTraits","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Jobs/Mailbox","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Console","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/front-end/src/composables","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Console/Commands/Calendars","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Http/Controllers/API","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Http/Controllers/Internal/WebhookReceiver","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/IntegrationApp/ServiceTraits","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/Queue","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Console/Commands/Crm/Hubspot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/Transcription/Job","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/tests/Unit/Services/Listeners","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/Listeners","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Traits","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/tests/Unit/Jobs/Crm/Hubspot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/tests/Unit/Services/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Activity","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Calendar/Command","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/.idea/queries","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/vendor/hubspot/api-client/codegen/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/vendor/hubspot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/Fields","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/Copper","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/Bullhorn","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Notifications/Channels","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/tests/Unit","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot/Journal","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Interactions/Settings/Teams","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Exceptions/Crm","depth":6,"on_screen":false,"role_description":"text"}]...
|
-7149045268642619196
|
-296379528057632829
|
click
|
accessibility
|
NULL
|
Find in Files
97 matches in 21 files
File mask:
*. Find in Files
97 matches in 21 files
File mask:
*.php
*.php
Auto
*.php
Filter Search Results
Pin Window
Search History
makeRequest
New Line
Match case
Words
Regex
Replace History
Replace
New Line
Preserve case
In Project
Module
Directory
Scope
Module
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot
/Users/lukas/jiminny/app/app/Exceptions
/Users/lukas/jiminny/app/app/Listeners/Crm
/Users/lukas/jiminny/app/app/Component/Queue/Job
/Users/lukas/jiminny/app/app/Listeners/AutomatedReports/UserPilot
/Users/lukas/jiminny/app/app/Events/Crm
/Users/lukas/jiminny/app/app/Jobs/AutomatedReports
/Users/lukas/jiminny/app/app/Listeners/Activities/Coaching/UserPilot
/Users/lukas/jiminny/app/app/Listeners/Activities/ActivityProvider/UserPilot
/Users/lukas/jiminny/app/app/Jobs/Activity/PushSummaryToCrm
/Users/lukas/jiminny/app/app/Repositories/Crm
/Users/lukas/jiminny/app/app/Jobs/Crm/Delete
/Users/lukas/jiminny/app/app/Services/Kiosk/AutomatedReports
/Users/lukas/jiminny/app/app/Http/Controllers/API/UserAutomatedReports
/Users/lukas/jiminny/app/app/Services/Crm/Salesforce
/Users/lukas/jiminny/app/app/Providers
/Users/lukas/jiminny/app/app/Services/Crm/IntegrationApp
/Users/lukas/jiminny/app/app/Events/Activities/Crm
/Users/lukas/jiminny/app/app/Listeners/Playbooks
/Users/lukas/jiminny/app/app/Console/Commands/Crm
/Users/lukas/jiminny/app/app/Services/Crm
/Users/lukas/jiminny/app/app/Http/Controllers
/Users/lukas/jiminny/app/app/Console/Commands/Reports
/Users/lukas/jiminny/app/app/VO/Repository/OnDemandActivitySearch
/Users/lukas/jiminny/app/app/Listeners/Activities/Conferences/UserPilot
/Users/lukas/jiminny/app/app/Http/Controllers/Webhook
/Users/lukas/jiminny/app/resources/views/emails/reports
/Users/lukas/jiminny/app/app/Mail/Reports
/Users/lukas/jiminny/app/app/Repositories
/Users/lukas/jiminny/app/app/Component/ActivitySearch/Service
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Salesforce
/Users/lukas/jiminny/app/routes
/Users/lukas/jiminny/app/app/Console/Commands
/Users/lukas/jiminny/app/database/migrations
/Users/lukas/Library/Application Support/JetBrains/PhpStorm2026.1/consoles/db/325d461a-c90f-430a-99d4-6ddfce0c61d7
/Users/lukas/jiminny/app/app/Http/Controllers/API/V2
/Users/lukas/jiminny/app/app/Jobs/Crm/Hubspot
/Users/lukas/jiminny/app/app/Component/DealInsights
/Users/lukas/jiminny/app/app/Policies
/Users/lukas/jiminny/app/app/Services/Crm/Helpers
/Users/lukas/jiminny/app/app/Jobs/Crm
/Users/lukas/jiminny/app/app/Models
/Users/lukas/jiminny/app/app/Listeners/Teams
/Users/lukas/jiminny/app/app/Jobs/Crm/Salesforce
/Users/lukas/jiminny/app/app
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/Journal
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/ServiceTraits
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/OpportunitySyncStrategy
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/Webhook
/Users/lukas/jiminny/app/storage/logs
/Users/lukas/jiminny/app
/Users/lukas/jiminny/app/app/Services/Internal
/Users/lukas/jiminny/app/app/Listeners/Transcription
/Users/lukas/jiminny/app/tests/Unit/Listeners/Teams
/Users/lukas/jiminny/app/app/Models/Crm
/Users/lukas/Library/Application Support/JetBrains/PhpStorm2026.1/consoles/db/91133dfa-8d71-4e12-bfb8-fec7f1afba8f
/Users/lukas/jiminny/app/app/Observers
/Users/lukas/jiminny/app/app/Services/Mail
/Users/lukas/jiminny/app/app/Console/Commands/Activities
/Users/lukas/jiminny/app/app/Console/Commands/Activities/Migrator
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot/ServiceTraits
/Users/lukas/jiminny/app/app/Jobs/User
/Users/lukas/jiminny/app/app/Models/Activity
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot/Webhook
/Users/lukas/jiminny/app/app/Component/AiAutomation/Listeners/PendingAnalysis
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot
/Users/lukas/jiminny/app/app/Component/ActivitySearch/FilterDefinition/DealInsights
/Users/lukas/jiminny/app/app/Services/Crm/DecorateActivity
/Users/lukas/jiminny/app/app/Component/Activity/Event
/Users/lukas/jiminny/app/app/Component/Sidekick
/Users/lukas/jiminny/app/app/Listeners/Activities/Conferences
/Users/lukas/jiminny/app/app/Listeners/Activities/Bots
/Users/lukas/jiminny/app/app/Services/RecallAI/Webhooks/Handlers
/Users/lukas/jiminny/app/app/Events/Activities/Bots
/Users/lukas/jiminny/app/app/Component/MeetingBot
/Users/lukas/jiminny/app/app/Services/Activity/RingCentral
/Users/lukas/jiminny/app/app/Http/Controllers/Webhook/Hubspot
/Users/lukas/jiminny/app/app/Services/Activity/Gmail
/Users/lukas/jiminny/app/app/Services/Crm/CrmObjects/ServiceTraits
/Users/lukas/jiminny/app/app/Jobs/Mailbox
/Users/lukas/jiminny/app/app/Console
/Users/lukas/jiminny/app/front-end/src/composables
/Users/lukas/jiminny/app/app/Console/Commands/Calendars
/Users/lukas/jiminny/app/app/Http/Controllers/API
/Users/lukas/jiminny/app/app/Http/Controllers/Internal/WebhookReceiver
/Users/lukas/jiminny/app/app/Services/Crm/IntegrationApp/ServiceTraits
/Users/lukas/jiminny/app/app/Component/Queue
/Users/lukas/jiminny/app/app/Console/Commands/Crm/Hubspot
/Users/lukas/jiminny/app/app/Component/Transcription/Job
/Users/lukas/jiminny/app/tests/Unit/Services/Listeners
/Users/lukas/jiminny/app/app/Services/Crm/Listeners
/Users/lukas/jiminny/app/app/Traits
/Users/lukas/jiminny/app/tests/Unit/Jobs/Crm/Hubspot
/Users/lukas/jiminny/app/tests/Unit/Services/Crm
/Users/lukas/jiminny/app/app/Services/Activity
/Users/lukas/jiminny/app/app/Services/Calendar/Command
/Users/lukas/jiminny/app/.idea/queries
/Users/lukas/jiminny/app/vendor/hubspot/api-client/codegen/Crm
/Users/lukas/jiminny/app/vendor/hubspot
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/Fields
/Users/lukas/jiminny/app/app/Services/Crm/Copper
/Users/lukas/jiminny/app/app/Services/Crm/Bullhorn
/Users/lukas/jiminny/app/app/Notifications/Channels
/Users/lukas/jiminny/app/tests/Unit
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot/Journal
/Users/lukas/jiminny/app/app/Interactions/Settings/Teams
/Users/lukas/jiminny/app/app/Exceptions/Crm...
|
2854
|
NULL
|
NULL
|
NULL
|
|
2856
|
113
|
44
|
2026-05-07T11:43:39.377312+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778154219377_m1.jpg...
|
PhpStorm
|
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Find in Files
97 matches in 21 files
File mask:
*. Find in Files
97 matches in 21 files
File mask:
*.php
*.php
Auto
*.php
Filter Search Results
Pin Window
Search History
makeRequest
New Line
Match case
Words
Regex
Replace History
Replace
New Line
Preserve case
In Project
Module
Directory
Scope
Module
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot
/Users/lukas/jiminny/app/app/Exceptions
/Users/lukas/jiminny/app/app/Listeners/Crm
/Users/lukas/jiminny/app/app/Component/Queue/Job
/Users/lukas/jiminny/app/app/Listeners/AutomatedReports/UserPilot
/Users/lukas/jiminny/app/app/Events/Crm
/Users/lukas/jiminny/app/app/Jobs/AutomatedReports
/Users/lukas/jiminny/app/app/Listeners/Activities/Coaching/UserPilot
/Users/lukas/jiminny/app/app/Listeners/Activities/ActivityProvider/UserPilot
/Users/lukas/jiminny/app/app/Jobs/Activity/PushSummaryToCrm
/Users/lukas/jiminny/app/app/Repositories/Crm
/Users/lukas/jiminny/app/app/Jobs/Crm/Delete
/Users/lukas/jiminny/app/app/Services/Kiosk/AutomatedReports
/Users/lukas/jiminny/app/app/Http/Controllers/API/UserAutomatedReports
/Users/lukas/jiminny/app/app/Services/Crm/Salesforce
/Users/lukas/jiminny/app/app/Providers
/Users/lukas/jiminny/app/app/Services/Crm/IntegrationApp
/Users/lukas/jiminny/app/app/Events/Activities/Crm
/Users/lukas/jiminny/app/app/Listeners/Playbooks
/Users/lukas/jiminny/app/app/Console/Commands/Crm
/Users/lukas/jiminny/app/app/Services/Crm
/Users/lukas/jiminny/app/app/Http/Controllers
/Users/lukas/jiminny/app/app/Console/Commands/Reports
/Users/lukas/jiminny/app/app/VO/Repository/OnDemandActivitySearch
/Users/lukas/jiminny/app/app/Listeners/Activities/Conferences/UserPilot
/Users/lukas/jiminny/app/app/Http/Controllers/Webhook
/Users/lukas/jiminny/app/resources/views/emails/reports
/Users/lukas/jiminny/app/app/Mail/Reports
/Users/lukas/jiminny/app/app/Repositories
/Users/lukas/jiminny/app/app/Component/ActivitySearch/Service
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Salesforce
/Users/lukas/jiminny/app/routes
/Users/lukas/jiminny/app/app/Console/Commands
/Users/lukas/jiminny/app/database/migrations
/Users/lukas/Library/Application Support/JetBrains/PhpStorm2026.1/consoles/db/325d461a-c90f-430a-99d4-6ddfce0c61d7
/Users/lukas/jiminny/app/app/Http/Controllers/API/V2
/Users/lukas/jiminny/app/app/Jobs/Crm/Hubspot
/Users/lukas/jiminny/app/app/Component/DealInsights
/Users/lukas/jiminny/app/app/Policies
/Users/lukas/jiminny/app/app/Services/Crm/Helpers
/Users/lukas/jiminny/app/app/Jobs/Crm
/Users/lukas/jiminny/app/app/Models
/Users/lukas/jiminny/app/app/Listeners/Teams
/Users/lukas/jiminny/app/app/Jobs/Crm/Salesforce
/Users/lukas/jiminny/app/app
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/Journal
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/ServiceTraits
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/OpportunitySyncStrategy
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/Webhook
/Users/lukas/jiminny/app/storage/logs
/Users/lukas/jiminny/app
/Users/lukas/jiminny/app/app/Services/Internal
/Users/lukas/jiminny/app/app/Listeners/Transcription
/Users/lukas/jiminny/app/tests/Unit/Listeners/Teams
/Users/lukas/jiminny/app/app/Models/Crm
/Users/lukas/Library/Application Support/JetBrains/PhpStorm2026.1/consoles/db/91133dfa-8d71-4e12-bfb8-fec7f1afba8f
/Users/lukas/jiminny/app/app/Observers
/Users/lukas/jiminny/app/app/Services/Mail
/Users/lukas/jiminny/app/app/Console/Commands/Activities
/Users/lukas/jiminny/app/app/Console/Commands/Activities/Migrator
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot/ServiceTraits
/Users/lukas/jiminny/app/app/Jobs/User
/Users/lukas/jiminny/app/app/Models/Activity
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot/Webhook
/Users/lukas/jiminny/app/app/Component/AiAutomation/Listeners/PendingAnalysis
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot
/Users/lukas/jiminny/app/app/Component/ActivitySearch/FilterDefinition/DealInsights
/Users/lukas/jiminny/app/app/Services/Crm/DecorateActivity
/Users/lukas/jiminny/app/app/Component/Activity/Event
/Users/lukas/jiminny/app/app/Component/Sidekick
/Users/lukas/jiminny/app/app/Listeners/Activities/Conferences
/Users/lukas/jiminny/app/app/Listeners/Activities/Bots
/Users/lukas/jiminny/app/app/Services/RecallAI/Webhooks/Handlers
/Users/lukas/jiminny/app/app/Events/Activities/Bots
/Users/lukas/jiminny/app/app/Component/MeetingBot
/Users/lukas/jiminny/app/app/Services/Activity/RingCentral
/Users/lukas/jiminny/app/app/Http/Controllers/Webhook/Hubspot
/Users/lukas/jiminny/app/app/Services/Activity/Gmail
/Users/lukas/jiminny/app/app/Services/Crm/CrmObjects/ServiceTraits
/Users/lukas/jiminny/app/app/Jobs/Mailbox
/Users/lukas/jiminny/app/app/Console
/Users/lukas/jiminny/app/front-end/src/composables
/Users/lukas/jiminny/app/app/Console/Commands/Calendars
/Users/lukas/jiminny/app/app/Http/Controllers/API
/Users/lukas/jiminny/app/app/Http/Controllers/Internal/WebhookReceiver
/Users/lukas/jiminny/app/app/Services/Crm/IntegrationApp/ServiceTraits
/Users/lukas/jiminny/app/app/Component/Queue
/Users/lukas/jiminny/app/app/Console/Commands/Crm/Hubspot
/Users/lukas/jiminny/app/app/Component/Transcription/Job
/Users/lukas/jiminny/app/tests/Unit/Services/Listeners
/Users/lukas/jiminny/app/app/Services/Crm/Listeners
/Users/lukas/jiminny/app/app/Traits
/Users/lukas/jiminny/app/tests/Unit/Jobs/Crm/Hubspot
/Users/lukas/jiminny/app/tests/Unit/Services/Crm
/Users/lukas/jiminny/app/app/Services/Activity
/Users/lukas/jiminny/app/app/Services/Calendar/Command
/Users/lukas/jiminny/app/.idea/queries
/Users/lukas/jiminny/app/vendor/hubspot/api-client/codegen/Crm
/Users/lukas/jiminny/app/vendor/hubspot
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/Fields
/Users/lukas/jiminny/app/app/Services/Crm/Copper
/Users/lukas/jiminny/app/app/Services/Crm/Bullhorn
/Users/lukas/jiminny/app/app/Notifications/Channels
/Users/lukas/jiminny/app/tests/Unit
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot/Journal
/Users/lukas/jiminny/app/app/Interactions/Settings/Teams
/Users/lukas/jiminny/app/app/Exceptions/Crm
/Users/lukas/jiminny/app/vendor/hubspot/hubspot-php/src/Endpoints
/Users/lukas/jiminny/app/config
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot/OpportunitySyncStrategy
/Users/lukas/jiminny/app/vendor/laravel/framework/src/Illuminate/Redis/Connections
/Users/lukas/jiminny/app/app/Http/Controllers/Settings/Teams
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/Webhook/Traits
/Users/lukas/jiminny/app/vendor/laravel/framework/src/Illuminate/Broadcasting
/Users/lukas/jiminny/app/app/Component/FeatureFlags...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"Find in Files","depth":1,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"97 matches in 21 files","depth":1,"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"File mask:","depth":1,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXComboBox","text":"*.php","depth":1,"on_screen":true,"value":"*.php","role_description":"combo box","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"*.php","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Auto","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXTextField","text":"*.php","depth":2,"on_screen":true,"value":"*.php","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Filter Search Results","depth":1,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Pin Window","depth":1,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Search History","depth":1,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"makeRequest","depth":2,"on_screen":true,"value":"makeRequest","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"New Line","depth":1,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Match case","depth":1,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Words","depth":1,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Regex","depth":1,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Replace History","depth":1,"bounds":{"left":0.0,"top":0.0,"width":0.015277778,"height":0.024444444},"on_screen":false,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"Replace","depth":2,"on_screen":false,"role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"New Line","depth":1,"bounds":{"left":0.0,"top":0.0,"width":0.015277778,"height":0.024444444},"on_screen":false,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Preserve case","depth":1,"bounds":{"left":0.0,"top":0.0,"width":0.015277778,"height":0.024444444},"on_screen":false,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"In Project","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Module","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Directory","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Scope","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"Module","depth":1,"bounds":{"left":0.0,"top":0.0,"width":0.20833333,"height":0.037777778},"on_screen":false,"role_description":"pop up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXComboBox","text":"/Users/lukas/jiminny/app/app/Services/Crm/Hubspot","depth":1,"bounds":{"left":0.0,"top":0.0,"width":0.4125,"height":0.037777778},"on_screen":false,"value":"/Users/lukas/jiminny/app/app/Services/Crm/Hubspot","role_description":"combo box","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/Hubspot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Exceptions","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/Queue/Job","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/AutomatedReports/UserPilot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Events/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Jobs/AutomatedReports","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/Activities/Coaching/UserPilot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/Activities/ActivityProvider/UserPilot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Jobs/Activity/PushSummaryToCrm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Repositories/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Jobs/Crm/Delete","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Kiosk/AutomatedReports","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Http/Controllers/API/UserAutomatedReports","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/Salesforce","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Providers","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/IntegrationApp","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Events/Activities/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/Playbooks","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Console/Commands/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Http/Controllers","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Console/Commands/Reports","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/VO/Repository/OnDemandActivitySearch","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/Activities/Conferences/UserPilot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Http/Controllers/Webhook","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/resources/views/emails/reports","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Mail/Reports","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Repositories","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/ActivitySearch/Service","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Salesforce","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/routes","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Console/Commands","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/database/migrations","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/Library/Application Support/JetBrains/PhpStorm2026.1/consoles/db/325d461a-c90f-430a-99d4-6ddfce0c61d7","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Http/Controllers/API/V2","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Jobs/Crm/Hubspot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/DealInsights","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Policies","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/Helpers","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Jobs/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Models","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/Teams","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Jobs/Crm/Salesforce","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/Journal","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/ServiceTraits","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/OpportunitySyncStrategy","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/Webhook","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/storage/logs","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Internal","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/Transcription","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/tests/Unit/Listeners/Teams","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Models/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/Library/Application Support/JetBrains/PhpStorm2026.1/consoles/db/91133dfa-8d71-4e12-bfb8-fec7f1afba8f","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Observers","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Mail","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Console/Commands/Activities","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Console/Commands/Activities/Migrator","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot/ServiceTraits","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Jobs/User","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Models/Activity","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot/Webhook","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/AiAutomation/Listeners/PendingAnalysis","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/ActivitySearch/FilterDefinition/DealInsights","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/DecorateActivity","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/Activity/Event","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/Sidekick","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/Activities/Conferences","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/Activities/Bots","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/RecallAI/Webhooks/Handlers","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Events/Activities/Bots","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/MeetingBot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Activity/RingCentral","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Http/Controllers/Webhook/Hubspot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Activity/Gmail","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/CrmObjects/ServiceTraits","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Jobs/Mailbox","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Console","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/front-end/src/composables","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Console/Commands/Calendars","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Http/Controllers/API","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Http/Controllers/Internal/WebhookReceiver","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/IntegrationApp/ServiceTraits","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/Queue","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Console/Commands/Crm/Hubspot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/Transcription/Job","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/tests/Unit/Services/Listeners","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/Listeners","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Traits","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/tests/Unit/Jobs/Crm/Hubspot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/tests/Unit/Services/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Activity","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Calendar/Command","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/.idea/queries","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/vendor/hubspot/api-client/codegen/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/vendor/hubspot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/Fields","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/Copper","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/Bullhorn","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Notifications/Channels","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/tests/Unit","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot/Journal","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Interactions/Settings/Teams","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Exceptions/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/vendor/hubspot/hubspot-php/src/Endpoints","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/config","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot/OpportunitySyncStrategy","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/vendor/laravel/framework/src/Illuminate/Redis/Connections","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Http/Controllers/Settings/Teams","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/Webhook/Traits","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/vendor/laravel/framework/src/Illuminate/Broadcasting","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/FeatureFlags","depth":6,"on_screen":false,"role_description":"text"}]...
|
6238481288680682243
|
-294057359500286269
|
click
|
accessibility
|
NULL
|
Find in Files
97 matches in 21 files
File mask:
*. Find in Files
97 matches in 21 files
File mask:
*.php
*.php
Auto
*.php
Filter Search Results
Pin Window
Search History
makeRequest
New Line
Match case
Words
Regex
Replace History
Replace
New Line
Preserve case
In Project
Module
Directory
Scope
Module
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot
/Users/lukas/jiminny/app/app/Exceptions
/Users/lukas/jiminny/app/app/Listeners/Crm
/Users/lukas/jiminny/app/app/Component/Queue/Job
/Users/lukas/jiminny/app/app/Listeners/AutomatedReports/UserPilot
/Users/lukas/jiminny/app/app/Events/Crm
/Users/lukas/jiminny/app/app/Jobs/AutomatedReports
/Users/lukas/jiminny/app/app/Listeners/Activities/Coaching/UserPilot
/Users/lukas/jiminny/app/app/Listeners/Activities/ActivityProvider/UserPilot
/Users/lukas/jiminny/app/app/Jobs/Activity/PushSummaryToCrm
/Users/lukas/jiminny/app/app/Repositories/Crm
/Users/lukas/jiminny/app/app/Jobs/Crm/Delete
/Users/lukas/jiminny/app/app/Services/Kiosk/AutomatedReports
/Users/lukas/jiminny/app/app/Http/Controllers/API/UserAutomatedReports
/Users/lukas/jiminny/app/app/Services/Crm/Salesforce
/Users/lukas/jiminny/app/app/Providers
/Users/lukas/jiminny/app/app/Services/Crm/IntegrationApp
/Users/lukas/jiminny/app/app/Events/Activities/Crm
/Users/lukas/jiminny/app/app/Listeners/Playbooks
/Users/lukas/jiminny/app/app/Console/Commands/Crm
/Users/lukas/jiminny/app/app/Services/Crm
/Users/lukas/jiminny/app/app/Http/Controllers
/Users/lukas/jiminny/app/app/Console/Commands/Reports
/Users/lukas/jiminny/app/app/VO/Repository/OnDemandActivitySearch
/Users/lukas/jiminny/app/app/Listeners/Activities/Conferences/UserPilot
/Users/lukas/jiminny/app/app/Http/Controllers/Webhook
/Users/lukas/jiminny/app/resources/views/emails/reports
/Users/lukas/jiminny/app/app/Mail/Reports
/Users/lukas/jiminny/app/app/Repositories
/Users/lukas/jiminny/app/app/Component/ActivitySearch/Service
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Salesforce
/Users/lukas/jiminny/app/routes
/Users/lukas/jiminny/app/app/Console/Commands
/Users/lukas/jiminny/app/database/migrations
/Users/lukas/Library/Application Support/JetBrains/PhpStorm2026.1/consoles/db/325d461a-c90f-430a-99d4-6ddfce0c61d7
/Users/lukas/jiminny/app/app/Http/Controllers/API/V2
/Users/lukas/jiminny/app/app/Jobs/Crm/Hubspot
/Users/lukas/jiminny/app/app/Component/DealInsights
/Users/lukas/jiminny/app/app/Policies
/Users/lukas/jiminny/app/app/Services/Crm/Helpers
/Users/lukas/jiminny/app/app/Jobs/Crm
/Users/lukas/jiminny/app/app/Models
/Users/lukas/jiminny/app/app/Listeners/Teams
/Users/lukas/jiminny/app/app/Jobs/Crm/Salesforce
/Users/lukas/jiminny/app/app
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/Journal
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/ServiceTraits
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/OpportunitySyncStrategy
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/Webhook
/Users/lukas/jiminny/app/storage/logs
/Users/lukas/jiminny/app
/Users/lukas/jiminny/app/app/Services/Internal
/Users/lukas/jiminny/app/app/Listeners/Transcription
/Users/lukas/jiminny/app/tests/Unit/Listeners/Teams
/Users/lukas/jiminny/app/app/Models/Crm
/Users/lukas/Library/Application Support/JetBrains/PhpStorm2026.1/consoles/db/91133dfa-8d71-4e12-bfb8-fec7f1afba8f
/Users/lukas/jiminny/app/app/Observers
/Users/lukas/jiminny/app/app/Services/Mail
/Users/lukas/jiminny/app/app/Console/Commands/Activities
/Users/lukas/jiminny/app/app/Console/Commands/Activities/Migrator
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot/ServiceTraits
/Users/lukas/jiminny/app/app/Jobs/User
/Users/lukas/jiminny/app/app/Models/Activity
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot/Webhook
/Users/lukas/jiminny/app/app/Component/AiAutomation/Listeners/PendingAnalysis
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot
/Users/lukas/jiminny/app/app/Component/ActivitySearch/FilterDefinition/DealInsights
/Users/lukas/jiminny/app/app/Services/Crm/DecorateActivity
/Users/lukas/jiminny/app/app/Component/Activity/Event
/Users/lukas/jiminny/app/app/Component/Sidekick
/Users/lukas/jiminny/app/app/Listeners/Activities/Conferences
/Users/lukas/jiminny/app/app/Listeners/Activities/Bots
/Users/lukas/jiminny/app/app/Services/RecallAI/Webhooks/Handlers
/Users/lukas/jiminny/app/app/Events/Activities/Bots
/Users/lukas/jiminny/app/app/Component/MeetingBot
/Users/lukas/jiminny/app/app/Services/Activity/RingCentral
/Users/lukas/jiminny/app/app/Http/Controllers/Webhook/Hubspot
/Users/lukas/jiminny/app/app/Services/Activity/Gmail
/Users/lukas/jiminny/app/app/Services/Crm/CrmObjects/ServiceTraits
/Users/lukas/jiminny/app/app/Jobs/Mailbox
/Users/lukas/jiminny/app/app/Console
/Users/lukas/jiminny/app/front-end/src/composables
/Users/lukas/jiminny/app/app/Console/Commands/Calendars
/Users/lukas/jiminny/app/app/Http/Controllers/API
/Users/lukas/jiminny/app/app/Http/Controllers/Internal/WebhookReceiver
/Users/lukas/jiminny/app/app/Services/Crm/IntegrationApp/ServiceTraits
/Users/lukas/jiminny/app/app/Component/Queue
/Users/lukas/jiminny/app/app/Console/Commands/Crm/Hubspot
/Users/lukas/jiminny/app/app/Component/Transcription/Job
/Users/lukas/jiminny/app/tests/Unit/Services/Listeners
/Users/lukas/jiminny/app/app/Services/Crm/Listeners
/Users/lukas/jiminny/app/app/Traits
/Users/lukas/jiminny/app/tests/Unit/Jobs/Crm/Hubspot
/Users/lukas/jiminny/app/tests/Unit/Services/Crm
/Users/lukas/jiminny/app/app/Services/Activity
/Users/lukas/jiminny/app/app/Services/Calendar/Command
/Users/lukas/jiminny/app/.idea/queries
/Users/lukas/jiminny/app/vendor/hubspot/api-client/codegen/Crm
/Users/lukas/jiminny/app/vendor/hubspot
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/Fields
/Users/lukas/jiminny/app/app/Services/Crm/Copper
/Users/lukas/jiminny/app/app/Services/Crm/Bullhorn
/Users/lukas/jiminny/app/app/Notifications/Channels
/Users/lukas/jiminny/app/tests/Unit
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot/Journal
/Users/lukas/jiminny/app/app/Interactions/Settings/Teams
/Users/lukas/jiminny/app/app/Exceptions/Crm
/Users/lukas/jiminny/app/vendor/hubspot/hubspot-php/src/Endpoints
/Users/lukas/jiminny/app/config
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot/OpportunitySyncStrategy
/Users/lukas/jiminny/app/vendor/laravel/framework/src/Illuminate/Redis/Connections
/Users/lukas/jiminny/app/app/Http/Controllers/Settings/Teams
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/Webhook/Traits
/Users/lukas/jiminny/app/vendor/laravel/framework/src/Illuminate/Broadcasting
/Users/lukas/jiminny/app/app/Component/FeatureFlags...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
2855
|
113
|
43
|
2026-05-07T11:43:37.887671+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778154217887_m1.jpg...
|
PhpStorm
|
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Find in Files
97 matches in 21 files
File mask:
*. Find in Files
97 matches in 21 files
File mask:
*.php
*.php
Auto
*.php
Filter Search Results
Pin Window
Search History
makeRequest
New Line
Match case
Words
Regex
Replace History
Replace
New Line
Preserve case
In Project
Module
Directory
Scope
Module
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot
/Users/lukas/jiminny/app/app/Exceptions
/Users/lukas/jiminny/app/app/Listeners/Crm
/Users/lukas/jiminny/app/app/Component/Queue/Job
/Users/lukas/jiminny/app/app/Listeners/AutomatedReports/UserPilot
/Users/lukas/jiminny/app/app/Events/Crm
/Users/lukas/jiminny/app/app/Jobs/AutomatedReports
/Users/lukas/jiminny/app/app/Listeners/Activities/Coaching/UserPilot
/Users/lukas/jiminny/app/app/Listeners/Activities/ActivityProvider/UserPilot
/Users/lukas/jiminny/app/app/Jobs/Activity/PushSummaryToCrm
/Users/lukas/jiminny/app/app/Repositories/Crm
/Users/lukas/jiminny/app/app/Jobs/Crm/Delete
/Users/lukas/jiminny/app/app/Services/Kiosk/AutomatedReports
/Users/lukas/jiminny/app/app/Http/Controllers/API/UserAutomatedReports
/Users/lukas/jiminny/app/app/Services/Crm/Salesforce
/Users/lukas/jiminny/app/app/Providers
/Users/lukas/jiminny/app/app/Services/Crm/IntegrationApp
/Users/lukas/jiminny/app/app/Events/Activities/Crm
/Users/lukas/jiminny/app/app/Listeners/Playbooks
/Users/lukas/jiminny/app/app/Console/Commands/Crm
/Users/lukas/jiminny/app/app/Services/Crm
/Users/lukas/jiminny/app/app/Http/Controllers
/Users/lukas/jiminny/app/app/Console/Commands/Reports
/Users/lukas/jiminny/app/app/VO/Repository/OnDemandActivitySearch
/Users/lukas/jiminny/app/app/Listeners/Activities/Conferences/UserPilot
/Users/lukas/jiminny/app/app/Http/Controllers/Webhook
/Users/lukas/jiminny/app/resources/views/emails/reports...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"Find in Files","depth":1,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"97 matches in 21 files","depth":1,"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"File mask:","depth":1,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXComboBox","text":"*.php","depth":1,"on_screen":true,"value":"*.php","role_description":"combo box","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"*.php","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Auto","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXTextField","text":"*.php","depth":2,"on_screen":true,"value":"*.php","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Filter Search Results","depth":1,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Pin Window","depth":1,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Search History","depth":1,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"makeRequest","depth":2,"on_screen":true,"value":"makeRequest","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"New Line","depth":1,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Match case","depth":1,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Words","depth":1,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Regex","depth":1,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Replace History","depth":1,"bounds":{"left":0.0,"top":0.0,"width":0.015277778,"height":0.024444444},"on_screen":false,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"Replace","depth":2,"on_screen":false,"role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"New Line","depth":1,"bounds":{"left":0.0,"top":0.0,"width":0.015277778,"height":0.024444444},"on_screen":false,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Preserve case","depth":1,"bounds":{"left":0.0,"top":0.0,"width":0.015277778,"height":0.024444444},"on_screen":false,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"In Project","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Module","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Directory","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Scope","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"Module","depth":1,"bounds":{"left":0.0,"top":0.0,"width":0.20833333,"height":0.037777778},"on_screen":false,"role_description":"pop up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXComboBox","text":"/Users/lukas/jiminny/app/app/Services/Crm/Hubspot","depth":1,"bounds":{"left":0.0,"top":0.0,"width":0.4125,"height":0.037777778},"on_screen":false,"value":"/Users/lukas/jiminny/app/app/Services/Crm/Hubspot","role_description":"combo box","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/Hubspot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Exceptions","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/Queue/Job","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/AutomatedReports/UserPilot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Events/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Jobs/AutomatedReports","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/Activities/Coaching/UserPilot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/Activities/ActivityProvider/UserPilot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Jobs/Activity/PushSummaryToCrm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Repositories/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Jobs/Crm/Delete","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Kiosk/AutomatedReports","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Http/Controllers/API/UserAutomatedReports","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/Salesforce","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Providers","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/IntegrationApp","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Events/Activities/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/Playbooks","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Console/Commands/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Http/Controllers","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Console/Commands/Reports","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/VO/Repository/OnDemandActivitySearch","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/Activities/Conferences/UserPilot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Http/Controllers/Webhook","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/resources/views/emails/reports","depth":6,"on_screen":false,"role_description":"text"}]...
|
-1563318650401350501
|
-882314081566108285
|
click
|
accessibility
|
NULL
|
Find in Files
97 matches in 21 files
File mask:
*. Find in Files
97 matches in 21 files
File mask:
*.php
*.php
Auto
*.php
Filter Search Results
Pin Window
Search History
makeRequest
New Line
Match case
Words
Regex
Replace History
Replace
New Line
Preserve case
In Project
Module
Directory
Scope
Module
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot
/Users/lukas/jiminny/app/app/Exceptions
/Users/lukas/jiminny/app/app/Listeners/Crm
/Users/lukas/jiminny/app/app/Component/Queue/Job
/Users/lukas/jiminny/app/app/Listeners/AutomatedReports/UserPilot
/Users/lukas/jiminny/app/app/Events/Crm
/Users/lukas/jiminny/app/app/Jobs/AutomatedReports
/Users/lukas/jiminny/app/app/Listeners/Activities/Coaching/UserPilot
/Users/lukas/jiminny/app/app/Listeners/Activities/ActivityProvider/UserPilot
/Users/lukas/jiminny/app/app/Jobs/Activity/PushSummaryToCrm
/Users/lukas/jiminny/app/app/Repositories/Crm
/Users/lukas/jiminny/app/app/Jobs/Crm/Delete
/Users/lukas/jiminny/app/app/Services/Kiosk/AutomatedReports
/Users/lukas/jiminny/app/app/Http/Controllers/API/UserAutomatedReports
/Users/lukas/jiminny/app/app/Services/Crm/Salesforce
/Users/lukas/jiminny/app/app/Providers
/Users/lukas/jiminny/app/app/Services/Crm/IntegrationApp
/Users/lukas/jiminny/app/app/Events/Activities/Crm
/Users/lukas/jiminny/app/app/Listeners/Playbooks
/Users/lukas/jiminny/app/app/Console/Commands/Crm
/Users/lukas/jiminny/app/app/Services/Crm
/Users/lukas/jiminny/app/app/Http/Controllers
/Users/lukas/jiminny/app/app/Console/Commands/Reports
/Users/lukas/jiminny/app/app/VO/Repository/OnDemandActivitySearch
/Users/lukas/jiminny/app/app/Listeners/Activities/Conferences/UserPilot
/Users/lukas/jiminny/app/app/Http/Controllers/Webhook
/Users/lukas/jiminny/app/resources/views/emails/reports...
|
2852
|
NULL
|
NULL
|
NULL
|
|
2854
|
114
|
43
|
2026-05-07T11:43:37.787881+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778154217787_m2.jpg...
|
PhpStorm
|
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Find in Files
97 matches in 21 files
File mask:
*. Find in Files
97 matches in 21 files
File mask:
*.php
*.php
Auto
*.php
Filter Search Results
Pin Window
Search History
makeRequest
New Line
Match case
Words
Regex
Replace History
Replace
New Line
Preserve case
In Project
Module
Directory
Scope
Module
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot
/Users/lukas/jiminny/app/app/Exceptions
/Users/lukas/jiminny/app/app/Listeners/Crm
/Users/lukas/jiminny/app/app/Component/Queue/Job
/Users/lukas/jiminny/app/app/Listeners/AutomatedReports/UserPilot
/Users/lukas/jiminny/app/app/Events/Crm
/Users/lukas/jiminny/app/app/Jobs/AutomatedReports
/Users/lukas/jiminny/app/app/Listeners/Activities/Coaching/UserPilot
/Users/lukas/jiminny/app/app/Listeners/Activities/ActivityProvider/UserPilot
/Users/lukas/jiminny/app/app/Jobs/Activity/PushSummaryToCrm
/Users/lukas/jiminny/app/app/Repositories/Crm
/Users/lukas/jiminny/app/app/Jobs/Crm/Delete
/Users/lukas/jiminny/app/app/Services/Kiosk/AutomatedReports
/Users/lukas/jiminny/app/app/Http/Controllers/API/UserAutomatedReports
/Users/lukas/jiminny/app/app/Services/Crm/Salesforce
/Users/lukas/jiminny/app/app/Providers
/Users/lukas/jiminny/app/app/Services/Crm/IntegrationApp
/Users/lukas/jiminny/app/app/Events/Activities/Crm
/Users/lukas/jiminny/app/app/Listeners/Playbooks
/Users/lukas/jiminny/app/app/Console/Commands/Crm
/Users/lukas/jiminny/app/app/Services/Crm
/Users/lukas/jiminny/app/app/Http/Controllers
/Users/lukas/jiminny/app/app/Console/Commands/Reports
/Users/lukas/jiminny/app/app/VO/Repository/OnDemandActivitySearch
/Users/lukas/jiminny/app/app/Listeners/Activities/Conferences/UserPilot
/Users/lukas/jiminny/app/app/Http/Controllers/Webhook
/Users/lukas/jiminny/app/resources/views/emails/reports
/Users/lukas/jiminny/app/app/Mail/Reports
/Users/lukas/jiminny/app/app/Repositories
/Users/lukas/jiminny/app/app/Component/ActivitySearch/Service
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Salesforce
/Users/lukas/jiminny/app/routes...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"Find in Files","depth":1,"bounds":{"left":0.2992021,"top":0.12609737,"width":0.024601065,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"97 matches in 21 files","depth":1,"bounds":{"left":0.32779256,"top":0.12609737,"width":0.044215426,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"File mask:","depth":1,"bounds":{"left":0.5315825,"top":0.12290503,"width":0.029587766,"height":0.019952115},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXComboBox","text":"*.php","depth":1,"bounds":{"left":0.5621675,"top":0.11971269,"width":0.027925532,"height":0.027134877},"on_screen":true,"value":"*.php","role_description":"combo box","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"*.php","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Auto","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXTextField","text":"*.php","depth":2,"bounds":{"left":0.5661569,"top":0.12609737,"width":0.011635638,"height":0.013567438},"on_screen":true,"value":"*.php","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Filter Search Results","depth":1,"bounds":{"left":0.5944149,"top":0.12290503,"width":0.00731383,"height":0.017557861},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Pin Window","depth":1,"bounds":{"left":0.6037234,"top":0.12290503,"width":0.00731383,"height":0.017557861},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Search History","depth":1,"bounds":{"left":0.2962101,"top":0.15403032,"width":0.00731383,"height":0.017557861},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"makeRequest","depth":2,"bounds":{"left":0.30718085,"top":0.15403032,"width":0.26196808,"height":0.017557861},"on_screen":true,"value":"makeRequest","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"New Line","depth":1,"bounds":{"left":0.578125,"top":0.15403032,"width":0.00731383,"height":0.017557861},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Match case","depth":1,"bounds":{"left":0.5880984,"top":0.15403032,"width":0.00731383,"height":0.017557861},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Words","depth":1,"bounds":{"left":0.59674203,"top":0.15403032,"width":0.00731383,"height":0.017557861},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Regex","depth":1,"bounds":{"left":0.60538566,"top":0.15403032,"width":0.00731383,"height":0.017557861},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Replace History","depth":1,"bounds":{"left":0.27027926,"top":1.0,"width":0.00731383,"height":0.0},"on_screen":false,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"Replace","depth":2,"on_screen":false,"role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"New Line","depth":1,"bounds":{"left":0.27027926,"top":1.0,"width":0.00731383,"height":0.0},"on_screen":false,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Preserve case","depth":1,"bounds":{"left":0.27027926,"top":1.0,"width":0.00731383,"height":0.0},"on_screen":false,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"In Project","depth":2,"bounds":{"left":0.2992021,"top":0.1867518,"width":0.022938829,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Module","depth":2,"bounds":{"left":0.32214096,"top":0.1867518,"width":0.019281914,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Directory","depth":2,"bounds":{"left":0.3414229,"top":0.1867518,"width":0.022606382,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Scope","depth":2,"bounds":{"left":0.36402926,"top":0.1867518,"width":0.017287234,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"Module","depth":1,"bounds":{"left":0.27027926,"top":1.0,"width":0.099734046,"height":0.0},"on_screen":false,"role_description":"pop up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXComboBox","text":"/Users/lukas/jiminny/app/app/Services/Crm/Hubspot","depth":1,"bounds":{"left":0.27027926,"top":1.0,"width":0.1974734,"height":0.0},"on_screen":false,"value":"/Users/lukas/jiminny/app/app/Services/Crm/Hubspot","role_description":"combo box","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/Hubspot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Exceptions","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/Queue/Job","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/AutomatedReports/UserPilot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Events/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Jobs/AutomatedReports","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/Activities/Coaching/UserPilot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/Activities/ActivityProvider/UserPilot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Jobs/Activity/PushSummaryToCrm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Repositories/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Jobs/Crm/Delete","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Kiosk/AutomatedReports","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Http/Controllers/API/UserAutomatedReports","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/Salesforce","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Providers","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/IntegrationApp","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Events/Activities/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/Playbooks","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Console/Commands/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Http/Controllers","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Console/Commands/Reports","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/VO/Repository/OnDemandActivitySearch","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/Activities/Conferences/UserPilot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Http/Controllers/Webhook","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/resources/views/emails/reports","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Mail/Reports","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Repositories","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/ActivitySearch/Service","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Salesforce","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/routes","depth":6,"on_screen":false,"role_description":"text"}]...
|
8175708703555658387
|
-882314071910824701
|
click
|
accessibility
|
NULL
|
Find in Files
97 matches in 21 files
File mask:
*. Find in Files
97 matches in 21 files
File mask:
*.php
*.php
Auto
*.php
Filter Search Results
Pin Window
Search History
makeRequest
New Line
Match case
Words
Regex
Replace History
Replace
New Line
Preserve case
In Project
Module
Directory
Scope
Module
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot
/Users/lukas/jiminny/app/app/Exceptions
/Users/lukas/jiminny/app/app/Listeners/Crm
/Users/lukas/jiminny/app/app/Component/Queue/Job
/Users/lukas/jiminny/app/app/Listeners/AutomatedReports/UserPilot
/Users/lukas/jiminny/app/app/Events/Crm
/Users/lukas/jiminny/app/app/Jobs/AutomatedReports
/Users/lukas/jiminny/app/app/Listeners/Activities/Coaching/UserPilot
/Users/lukas/jiminny/app/app/Listeners/Activities/ActivityProvider/UserPilot
/Users/lukas/jiminny/app/app/Jobs/Activity/PushSummaryToCrm
/Users/lukas/jiminny/app/app/Repositories/Crm
/Users/lukas/jiminny/app/app/Jobs/Crm/Delete
/Users/lukas/jiminny/app/app/Services/Kiosk/AutomatedReports
/Users/lukas/jiminny/app/app/Http/Controllers/API/UserAutomatedReports
/Users/lukas/jiminny/app/app/Services/Crm/Salesforce
/Users/lukas/jiminny/app/app/Providers
/Users/lukas/jiminny/app/app/Services/Crm/IntegrationApp
/Users/lukas/jiminny/app/app/Events/Activities/Crm
/Users/lukas/jiminny/app/app/Listeners/Playbooks
/Users/lukas/jiminny/app/app/Console/Commands/Crm
/Users/lukas/jiminny/app/app/Services/Crm
/Users/lukas/jiminny/app/app/Http/Controllers
/Users/lukas/jiminny/app/app/Console/Commands/Reports
/Users/lukas/jiminny/app/app/VO/Repository/OnDemandActivitySearch
/Users/lukas/jiminny/app/app/Listeners/Activities/Conferences/UserPilot
/Users/lukas/jiminny/app/app/Http/Controllers/Webhook
/Users/lukas/jiminny/app/resources/views/emails/reports
/Users/lukas/jiminny/app/app/Mail/Reports
/Users/lukas/jiminny/app/app/Repositories
/Users/lukas/jiminny/app/app/Component/ActivitySearch/Service
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Salesforce
/Users/lukas/jiminny/app/routes...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
2853
|
114
|
42
|
2026-05-07T11:43:28.483601+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778154208483_m2.jpg...
|
PhpStorm
|
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Find in Files
97 matches in 21 files
File mask:
*. Find in Files
97 matches in 21 files
File mask:
*.php
*.php
Auto
*.php
Filter Search Results
Pin Window
Search History
makeRequest
New Line
Match case
Words
Regex
Replace History
Replace
New Line
Preserve case
In Project
Module
Directory
Scope
Module
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot
/Users/lukas/jiminny/app/app/Exceptions
/Users/lukas/jiminny/app/app/Listeners/Crm
/Users/lukas/jiminny/app/app/Component/Queue/Job
/Users/lukas/jiminny/app/app/Listeners/AutomatedReports/UserPilot
/Users/lukas/jiminny/app/app/Events/Crm
/Users/lukas/jiminny/app/app/Jobs/AutomatedReports
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot
Select Directory
Scope
Edit Scopes
Usages
$response = $client->makeRequest($endpoint, 'GET', [], $queryString); in FetchMergedObjectsPageJob.php 341
public function canMakeRequest(RateLimited $provider): bool in ProviderRateLimiter.php 20
private function makeRequest(int &$page): \GuzzleHttp\Promise\PromiseInterface|\Illuminate\Http\Client\Response in MigrateFromLeexiCommand.php 30
$response = $this->makeRequest($page); in MigrateFromLeexiCommand.php 51...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"Find in Files","depth":1,"bounds":{"left":0.2992021,"top":0.12609737,"width":0.024601065,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"97 matches in 21 files","depth":1,"bounds":{"left":0.32779256,"top":0.12609737,"width":0.044215426,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"File mask:","depth":1,"bounds":{"left":0.5315825,"top":0.12290503,"width":0.029587766,"height":0.019952115},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXComboBox","text":"*.php","depth":1,"bounds":{"left":0.5621675,"top":0.11971269,"width":0.027925532,"height":0.027134877},"on_screen":true,"value":"*.php","role_description":"combo box","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"*.php","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Auto","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXTextField","text":"*.php","depth":2,"bounds":{"left":0.5661569,"top":0.12609737,"width":0.011635638,"height":0.013567438},"on_screen":true,"value":"*.php","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Filter Search Results","depth":1,"bounds":{"left":0.5944149,"top":0.12290503,"width":0.00731383,"height":0.017557861},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Pin Window","depth":1,"bounds":{"left":0.6037234,"top":0.12290503,"width":0.00731383,"height":0.017557861},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Search History","depth":1,"bounds":{"left":0.2962101,"top":0.15403032,"width":0.00731383,"height":0.017557861},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"makeRequest","depth":2,"bounds":{"left":0.30718085,"top":0.15403032,"width":0.26196808,"height":0.017557861},"on_screen":true,"value":"makeRequest","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"New Line","depth":1,"bounds":{"left":0.578125,"top":0.15403032,"width":0.00731383,"height":0.017557861},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Match case","depth":1,"bounds":{"left":0.5880984,"top":0.15403032,"width":0.00731383,"height":0.017557861},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Words","depth":1,"bounds":{"left":0.59674203,"top":0.15403032,"width":0.00731383,"height":0.017557861},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Regex","depth":1,"bounds":{"left":0.60538566,"top":0.15403032,"width":0.00731383,"height":0.017557861},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Replace History","depth":1,"bounds":{"left":0.27027926,"top":1.0,"width":0.00731383,"height":0.0},"on_screen":false,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"Replace","depth":2,"on_screen":false,"role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"New Line","depth":1,"bounds":{"left":0.27027926,"top":1.0,"width":0.00731383,"height":0.0},"on_screen":false,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Preserve case","depth":1,"bounds":{"left":0.27027926,"top":1.0,"width":0.00731383,"height":0.0},"on_screen":false,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"In Project","depth":2,"bounds":{"left":0.2992021,"top":0.1867518,"width":0.022938829,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Module","depth":2,"bounds":{"left":0.32214096,"top":0.1867518,"width":0.019281914,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Directory","depth":2,"bounds":{"left":0.3414229,"top":0.1867518,"width":0.022606382,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Scope","depth":2,"bounds":{"left":0.36402926,"top":0.1867518,"width":0.017287234,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"Module","depth":1,"bounds":{"left":0.27027926,"top":1.0,"width":0.099734046,"height":0.0},"on_screen":false,"role_description":"pop up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXComboBox","text":"/Users/lukas/jiminny/app/app/Services/Crm/Hubspot","depth":1,"bounds":{"left":0.27027926,"top":1.0,"width":0.1974734,"height":0.0},"on_screen":false,"value":"/Users/lukas/jiminny/app/app/Services/Crm/Hubspot","role_description":"combo box","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/Hubspot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Exceptions","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/Queue/Job","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/AutomatedReports/UserPilot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Events/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Jobs/AutomatedReports","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXTextField","text":"/Users/lukas/jiminny/app/app/Services/Crm/Hubspot","depth":2,"bounds":{"left":0.27027926,"top":1.0,"width":0.18118352,"height":0.0},"on_screen":false,"value":"/Users/lukas/jiminny/app/app/Services/Crm/Hubspot","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Select Directory","depth":1,"bounds":{"left":0.27027926,"top":1.0,"width":0.011303191,"height":0.0},"on_screen":false,"help_text":"⇧⏎","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"Scope","depth":1,"bounds":{"left":0.27027926,"top":1.0,"width":0.099734046,"height":0.0},"on_screen":false,"role_description":"pop up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Edit Scopes","depth":1,"bounds":{"left":0.27027926,"top":1.0,"width":0.011303191,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Usages","depth":2,"on_screen":false,"role_description":"text"},{"role":"AXCell","text":"$response = $client->makeRequest($endpoint, 'GET', [], $queryString); in FetchMergedObjectsPageJob.php 341","depth":4,"bounds":{"left":0.2925532,"top":0.21069433,"width":0.32347074,"height":0.017557861},"on_screen":true,"role_description":"cell"},{"role":"AXCell","text":"public function canMakeRequest(RateLimited $provider): bool in ProviderRateLimiter.php 20","depth":4,"bounds":{"left":0.2925532,"top":0.22825219,"width":0.32347074,"height":0.017557861},"on_screen":true,"role_description":"cell"},{"role":"AXCell","text":"private function makeRequest(int &$page): \\GuzzleHttp\\Promise\\PromiseInterface|\\Illuminate\\Http\\Client\\Response in MigrateFromLeexiCommand.php 30","depth":4,"bounds":{"left":0.2925532,"top":0.24581006,"width":0.32347074,"height":0.017557861},"on_screen":true,"role_description":"cell"},{"role":"AXCell","text":"$response = $this->makeRequest($page); in MigrateFromLeexiCommand.php 51","depth":4,"bounds":{"left":0.2925532,"top":0.26336792,"width":0.32347074,"height":0.017557861},"on_screen":true,"role_description":"cell"}]...
|
4870806898310981754
|
-299229880537236150
|
click
|
accessibility
|
NULL
|
Find in Files
97 matches in 21 files
File mask:
*. Find in Files
97 matches in 21 files
File mask:
*.php
*.php
Auto
*.php
Filter Search Results
Pin Window
Search History
makeRequest
New Line
Match case
Words
Regex
Replace History
Replace
New Line
Preserve case
In Project
Module
Directory
Scope
Module
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot
/Users/lukas/jiminny/app/app/Exceptions
/Users/lukas/jiminny/app/app/Listeners/Crm
/Users/lukas/jiminny/app/app/Component/Queue/Job
/Users/lukas/jiminny/app/app/Listeners/AutomatedReports/UserPilot
/Users/lukas/jiminny/app/app/Events/Crm
/Users/lukas/jiminny/app/app/Jobs/AutomatedReports
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot
Select Directory
Scope
Edit Scopes
Usages
$response = $client->makeRequest($endpoint, 'GET', [], $queryString); in FetchMergedObjectsPageJob.php 341
public function canMakeRequest(RateLimited $provider): bool in ProviderRateLimiter.php 20
private function makeRequest(int &$page): \GuzzleHttp\Promise\PromiseInterface|\Illuminate\Http\Client\Response in MigrateFromLeexiCommand.php 30
$response = $this->makeRequest($page); in MigrateFromLeexiCommand.php 51...
|
2851
|
NULL
|
NULL
|
NULL
|
|
2852
|
113
|
42
|
2026-05-07T11:43:28.483575+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778154208483_m1.jpg...
|
PhpStorm
|
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Find in Files
97 matches in 21 files
File mask:
*. Find in Files
97 matches in 21 files
File mask:
*.php
*.php
Auto
*.php
Filter Search Results
Pin Window
Search History
makeRequest
New Line
Match case
Words
Regex
Replace History
Replace
New Line
Preserve case
In Project
Module...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"Find in Files","depth":1,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"97 matches in 21 files","depth":1,"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"File mask:","depth":1,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXComboBox","text":"*.php","depth":1,"on_screen":true,"value":"*.php","role_description":"combo box","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"*.php","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Auto","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXTextField","text":"*.php","depth":2,"on_screen":true,"value":"*.php","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Filter Search Results","depth":1,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Pin Window","depth":1,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Search History","depth":1,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"makeRequest","depth":2,"on_screen":true,"value":"makeRequest","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"New Line","depth":1,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Match case","depth":1,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Words","depth":1,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Regex","depth":1,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Replace History","depth":1,"bounds":{"left":0.0,"top":0.0,"width":0.015277778,"height":0.024444444},"on_screen":false,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"Replace","depth":2,"on_screen":false,"role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"New Line","depth":1,"bounds":{"left":0.0,"top":0.0,"width":0.015277778,"height":0.024444444},"on_screen":false,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Preserve case","depth":1,"bounds":{"left":0.0,"top":0.0,"width":0.015277778,"height":0.024444444},"on_screen":false,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"In Project","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Module","depth":2,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
7924345963505904320
|
703515919336445515
|
click
|
accessibility
|
NULL
|
Find in Files
97 matches in 21 files
File mask:
*. Find in Files
97 matches in 21 files
File mask:
*.php
*.php
Auto
*.php
Filter Search Results
Pin Window
Search History
makeRequest
New Line
Match case
Words
Regex
Replace History
Replace
New Line
Preserve case
In Project
Module...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
2851
|
114
|
41
|
2026-05-07T11:43:25.871128+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778154205871_m2.jpg...
|
PhpStorm
|
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Find in Files
97 matches in 21 files
File mask:
*. Find in Files
97 matches in 21 files
File mask:
*.php
*.php
Auto
*.php
Filter Search Results
Pin Window
Search History
makeRequest
New Line
Match case
Words
Regex
Replace History
Replace
New Line
Preserve case
In Project
Module
Directory
Scope
Module
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot
/Users/lukas/jiminny/app/app/Exceptions
/Users/lukas/jiminny/app/app/Listeners/Crm
/Users/lukas/jiminny/app/app/Component/Queue/Job
/Users/lukas/jiminny/app/app/Listeners/AutomatedReports/UserPilot
/Users/lukas/jiminny/app/app/Events/Crm
/Users/lukas/jiminny/app/app/Jobs/AutomatedReports
/Users/lukas/jiminny/app/app/Listeners/Activities/Coaching/UserPilot
/Users/lukas/jiminny/app/app/Listeners/Activities/ActivityProvider/UserPilot
/Users/lukas/jiminny/app/app/Jobs/Activity/PushSummaryToCrm
/Users/lukas/jiminny/app/app/Repositories/Crm
/Users/lukas/jiminny/app/app/Jobs/Crm/Delete
/Users/lukas/jiminny/app/app/Services/Kiosk/AutomatedReports
/Users/lukas/jiminny/app/app/Http/Controllers/API/UserAutomatedReports
/Users/lukas/jiminny/app/app/Services/Crm/Salesforce
/Users/lukas/jiminny/app/app/Providers
/Users/lukas/jiminny/app/app/Services/Crm/IntegrationApp
/Users/lukas/jiminny/app/app/Events/Activities/Crm
/Users/lukas/jiminny/app/app/Listeners/Playbooks
/Users/lukas/jiminny/app/app/Console/Commands/Crm
/Users/lukas/jiminny/app/app/Services/Crm
/Users/lukas/jiminny/app/app/Http/Controllers
/Users/lukas/jiminny/app/app/Console/Commands/Reports
/Users/lukas/jiminny/app/app/VO/Repository/OnDemandActivitySearch
/Users/lukas/jiminny/app/app/Listeners/Activities/Conferences/UserPilot
/Users/lukas/jiminny/app/app/Http/Controllers/Webhook
/Users/lukas/jiminny/app/resources/views/emails/reports
/Users/lukas/jiminny/app/app/Mail/Reports
/Users/lukas/jiminny/app/app/Repositories
/Users/lukas/jiminny/app/app/Component/ActivitySearch/Service
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Salesforce
/Users/lukas/jiminny/app/routes
/Users/lukas/jiminny/app/app/Console/Commands
/Users/lukas/jiminny/app/database/migrations
/Users/lukas/Library/Application Support/JetBrains/PhpStorm2026.1/consoles/db/325d461a-c90f-430a-99d4-6ddfce0c61d7
/Users/lukas/jiminny/app/app/Http/Controllers/API/V2
/Users/lukas/jiminny/app/app/Jobs/Crm/Hubspot
/Users/lukas/jiminny/app/app/Component/DealInsights
/Users/lukas/jiminny/app/app/Policies
/Users/lukas/jiminny/app/app/Services/Crm/Helpers
/Users/lukas/jiminny/app/app/Jobs/Crm
/Users/lukas/jiminny/app/app/Models
/Users/lukas/jiminny/app/app/Listeners/Teams
/Users/lukas/jiminny/app/app/Jobs/Crm/Salesforce
/Users/lukas/jiminny/app/app
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/Journal
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/ServiceTraits
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/OpportunitySyncStrategy
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/Webhook
/Users/lukas/jiminny/app/storage/logs
/Users/lukas/jiminny/app
/Users/lukas/jiminny/app/app/Services/Internal
/Users/lukas/jiminny/app/app/Listeners/Transcription
/Users/lukas/jiminny/app/tests/Unit/Listeners/Teams
/Users/lukas/jiminny/app/app/Models/Crm
/Users/lukas/Library/Application Support/JetBrains/PhpStorm2026.1/consoles/db/91133dfa-8d71-4e12-bfb8-fec7f1afba8f
/Users/lukas/jiminny/app/app/Observers
/Users/lukas/jiminny/app/app/Services/Mail
/Users/lukas/jiminny/app/app/Console/Commands/Activities
/Users/lukas/jiminny/app/app/Console/Commands/Activities/Migrator
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot/ServiceTraits
/Users/lukas/jiminny/app/app/Jobs/User
/Users/lukas/jiminny/app/app/Models/Activity
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot/Webhook
/Users/lukas/jiminny/app/app/Component/AiAutomation/Listeners/PendingAnalysis
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot
/Users/lukas/jiminny/app/app/Component/ActivitySearch/FilterDefinition/DealInsights
/Users/lukas/jiminny/app/app/Services/Crm/DecorateActivity
/Users/lukas/jiminny/app/app/Component/Activity/Event
/Users/lukas/jiminny/app/app/Component/Sidekick
/Users/lukas/jiminny/app/app/Listeners/Activities/Conferences
/Users/lukas/jiminny/app/app/Listeners/Activities/Bots
/Users/lukas/jiminny/app/app/Services/RecallAI/Webhooks/Handlers
/Users/lukas/jiminny/app/app/Events/Activities/Bots
/Users/lukas/jiminny/app/app/Component/MeetingBot
/Users/lukas/jiminny/app/app/Services/Activity/RingCentral
/Users/lukas/jiminny/app/app/Http/Controllers/Webhook/Hubspot
/Users/lukas/jiminny/app/app/Services/Activity/Gmail
/Users/lukas/jiminny/app/app/Services/Crm/CrmObjects/ServiceTraits
/Users/lukas/jiminny/app/app/Jobs/Mailbox
/Users/lukas/jiminny/app/app/Console
/Users/lukas/jiminny/app/front-end/src/composables
/Users/lukas/jiminny/app/app/Console/Commands/Calendars
/Users/lukas/jiminny/app/app/Http/Controllers/API
/Users/lukas/jiminny/app/app/Http/Controllers/Internal/WebhookReceiver
/Users/lukas/jiminny/app/app/Services/Crm/IntegrationApp/ServiceTraits
/Users/lukas/jiminny/app/app/Component/Queue
/Users/lukas/jiminny/app/app/Console/Commands/Crm/Hubspot
/Users/lukas/jiminny/app/app/Component/Transcription/Job
/Users/lukas/jiminny/app/tests/Unit/Services/Listeners
/Users/lukas/jiminny/app/app/Services/Crm/Listeners
/Users/lukas/jiminny/app/app/Traits
/Users/lukas/jiminny/app/tests/Unit/Jobs/Crm/Hubspot
/Users/lukas/jiminny/app/tests/Unit/Services/Crm
/Users/lukas/jiminny/app/app/Services/Activity
/Users/lukas/jiminny/app/app/Services/Calendar/Command
/Users/lukas/jiminny/app/.idea/queries
/Users/lukas/jiminny/app/vendor/hubspot/api-client/codegen/Crm
/Users/lukas/jiminny/app/vendor/hubspot
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/Fields
/Users/lukas/jiminny/app/app/Services/Crm/Copper
/Users/lukas/jiminny/app/app/Services/Crm/Bullhorn
/Users/lukas/jiminny/app/app/Notifications/Channels
/Users/lukas/jiminny/app/tests/Unit
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot/Journal
/Users/lukas/jiminny/app/app/Interactions/Settings/Teams
/Users/lukas/jiminny/app/app/Exceptions/Crm
/Users/lukas/jiminny/app/vendor/hubspot/hubspot-php/src/Endpoints
/Users/lukas/jiminny/app/config
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot/OpportunitySyncStrategy
/Users/lukas/jiminny/app/vendor/laravel/framework/src/Illuminate/Redis/Connections
/Users/lukas/jiminny/app/app/Http/Controllers/Settings/Teams
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/Webhook/Traits
/Users/lukas/jiminny/app/vendor/laravel/framework/src/Illuminate/Broadcasting
/Users/lukas/jiminny/app/app/Component/FeatureFlags
/Users/lukas/jiminny/app/app/Component/Activity
/Users/lukas/jiminny/app/app/Component/ActivitySearch
/Users/lukas/jiminny/app/tests/Unit/Events/Activities/Crm
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot/Pagination
/Users/lukas/jiminny/app/app/Console/Commands/Dev
/Users/lukas/jiminny/app/front-end
/Users/lukas/jiminny/app/app/Component/Prophet
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/IntegrationApp
/Users/lukas/jiminny/app/app/Component/AskAnything
/Users/lukas/jiminny/app/app/Component/AskJiminnyAi/OnDemandLevel/Events
/Users/lukas/jiminny/app/app/Component/AskAnything/Events
/Users/lukas/jiminny/app/app/Component/AskJiminnyAi/DealLevel/Traits
/Users/lukas/jiminny/app/app/Component/ProphetAi...
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"Find in Files","depth":1,"bounds":{"left":0.2992021,"top":0.12609737,"width":0.024601065,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"97 matches in 21 files","depth":1,"bounds":{"left":0.32779256,"top":0.12609737,"width":0.044215426,"height":0.013567438},"on_screen":true,"role_description":"text"},{"role":"AXCheckBox","text":"File mask:","depth":1,"bounds":{"left":0.5315825,"top":0.12290503,"width":0.029587766,"height":0.019952115},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXComboBox","text":"*.php","depth":1,"bounds":{"left":0.5621675,"top":0.11971269,"width":0.027925532,"height":0.027134877},"on_screen":true,"value":"*.php","role_description":"combo box","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"*.php","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Auto","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXTextField","text":"*.php","depth":2,"bounds":{"left":0.5661569,"top":0.12609737,"width":0.011635638,"height":0.013567438},"on_screen":true,"value":"*.php","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Filter Search Results","depth":1,"bounds":{"left":0.5944149,"top":0.12290503,"width":0.00731383,"height":0.017557861},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Pin Window","depth":1,"bounds":{"left":0.6037234,"top":0.12290503,"width":0.00731383,"height":0.017557861},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Search History","depth":1,"bounds":{"left":0.2962101,"top":0.15403032,"width":0.00731383,"height":0.017557861},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"makeRequest","depth":2,"bounds":{"left":0.30718085,"top":0.15403032,"width":0.26196808,"height":0.017557861},"on_screen":true,"value":"makeRequest","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"New Line","depth":1,"bounds":{"left":0.578125,"top":0.15403032,"width":0.00731383,"height":0.017557861},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Match case","depth":1,"bounds":{"left":0.5880984,"top":0.15403032,"width":0.00731383,"height":0.017557861},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Words","depth":1,"bounds":{"left":0.59674203,"top":0.15403032,"width":0.00731383,"height":0.017557861},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Regex","depth":1,"bounds":{"left":0.60538566,"top":0.15403032,"width":0.00731383,"height":0.017557861},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Replace History","depth":1,"bounds":{"left":0.27027926,"top":1.0,"width":0.00731383,"height":0.0},"on_screen":false,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"Replace","depth":2,"on_screen":false,"role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"New Line","depth":1,"bounds":{"left":0.27027926,"top":1.0,"width":0.00731383,"height":0.0},"on_screen":false,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Preserve case","depth":1,"bounds":{"left":0.27027926,"top":1.0,"width":0.00731383,"height":0.0},"on_screen":false,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"In Project","depth":2,"bounds":{"left":0.2992021,"top":0.1867518,"width":0.022938829,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Module","depth":2,"bounds":{"left":0.32214096,"top":0.1867518,"width":0.019281914,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Directory","depth":2,"bounds":{"left":0.3414229,"top":0.1867518,"width":0.022606382,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Scope","depth":2,"bounds":{"left":0.36402926,"top":0.1867518,"width":0.017287234,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXPopUpButton","text":"Module","depth":1,"bounds":{"left":0.27027926,"top":1.0,"width":0.099734046,"height":0.0},"on_screen":false,"role_description":"pop up button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXComboBox","text":"/Users/lukas/jiminny/app/app/Services/Crm/Hubspot","depth":1,"bounds":{"left":0.27027926,"top":1.0,"width":0.1974734,"height":0.0},"on_screen":false,"value":"/Users/lukas/jiminny/app/app/Services/Crm/Hubspot","role_description":"combo box","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/Hubspot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Exceptions","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/Queue/Job","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/AutomatedReports/UserPilot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Events/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Jobs/AutomatedReports","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/Activities/Coaching/UserPilot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/Activities/ActivityProvider/UserPilot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Jobs/Activity/PushSummaryToCrm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Repositories/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Jobs/Crm/Delete","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Kiosk/AutomatedReports","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Http/Controllers/API/UserAutomatedReports","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/Salesforce","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Providers","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/IntegrationApp","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Events/Activities/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/Playbooks","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Console/Commands/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Http/Controllers","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Console/Commands/Reports","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/VO/Repository/OnDemandActivitySearch","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/Activities/Conferences/UserPilot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Http/Controllers/Webhook","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/resources/views/emails/reports","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Mail/Reports","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Repositories","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/ActivitySearch/Service","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Salesforce","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/routes","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Console/Commands","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/database/migrations","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/Library/Application Support/JetBrains/PhpStorm2026.1/consoles/db/325d461a-c90f-430a-99d4-6ddfce0c61d7","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Http/Controllers/API/V2","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Jobs/Crm/Hubspot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/DealInsights","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Policies","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/Helpers","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Jobs/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Models","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/Teams","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Jobs/Crm/Salesforce","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/Journal","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/ServiceTraits","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/OpportunitySyncStrategy","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/Webhook","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/storage/logs","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Internal","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/Transcription","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/tests/Unit/Listeners/Teams","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Models/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/Library/Application Support/JetBrains/PhpStorm2026.1/consoles/db/91133dfa-8d71-4e12-bfb8-fec7f1afba8f","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Observers","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Mail","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Console/Commands/Activities","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Console/Commands/Activities/Migrator","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot/ServiceTraits","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Jobs/User","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Models/Activity","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot/Webhook","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/AiAutomation/Listeners/PendingAnalysis","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/ActivitySearch/FilterDefinition/DealInsights","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/DecorateActivity","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/Activity/Event","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/Sidekick","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/Activities/Conferences","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Listeners/Activities/Bots","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/RecallAI/Webhooks/Handlers","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Events/Activities/Bots","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/MeetingBot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Activity/RingCentral","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Http/Controllers/Webhook/Hubspot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Activity/Gmail","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/CrmObjects/ServiceTraits","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Jobs/Mailbox","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Console","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/front-end/src/composables","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Console/Commands/Calendars","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Http/Controllers/API","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Http/Controllers/Internal/WebhookReceiver","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/IntegrationApp/ServiceTraits","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/Queue","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Console/Commands/Crm/Hubspot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/Transcription/Job","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/tests/Unit/Services/Listeners","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/Listeners","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Traits","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/tests/Unit/Jobs/Crm/Hubspot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/tests/Unit/Services/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Activity","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Calendar/Command","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/.idea/queries","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/vendor/hubspot/api-client/codegen/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/vendor/hubspot","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/Fields","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/Copper","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/Bullhorn","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Notifications/Channels","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/tests/Unit","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot/Journal","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Interactions/Settings/Teams","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Exceptions/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/vendor/hubspot/hubspot-php/src/Endpoints","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/config","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot/OpportunitySyncStrategy","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/vendor/laravel/framework/src/Illuminate/Redis/Connections","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Http/Controllers/Settings/Teams","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/Webhook/Traits","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/vendor/laravel/framework/src/Illuminate/Broadcasting","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/FeatureFlags","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/Activity","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/ActivitySearch","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/tests/Unit/Events/Activities/Crm","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot/Pagination","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Console/Commands/Dev","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/front-end","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/Prophet","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/tests/Unit/Services/Crm/IntegrationApp","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/AskAnything","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/AskJiminnyAi/OnDemandLevel/Events","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/AskAnything/Events","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/AskJiminnyAi/DealLevel/Traits","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"/Users/lukas/jiminny/app/app/Component/ProphetAi","depth":6,"on_screen":false,"role_description":"text"}]...
|
-4681725591604833536
|
-294057363795253693
|
visual_change
|
accessibility
|
NULL
|
Find in Files
97 matches in 21 files
File mask:
*. Find in Files
97 matches in 21 files
File mask:
*.php
*.php
Auto
*.php
Filter Search Results
Pin Window
Search History
makeRequest
New Line
Match case
Words
Regex
Replace History
Replace
New Line
Preserve case
In Project
Module
Directory
Scope
Module
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot
/Users/lukas/jiminny/app/app/Exceptions
/Users/lukas/jiminny/app/app/Listeners/Crm
/Users/lukas/jiminny/app/app/Component/Queue/Job
/Users/lukas/jiminny/app/app/Listeners/AutomatedReports/UserPilot
/Users/lukas/jiminny/app/app/Events/Crm
/Users/lukas/jiminny/app/app/Jobs/AutomatedReports
/Users/lukas/jiminny/app/app/Listeners/Activities/Coaching/UserPilot
/Users/lukas/jiminny/app/app/Listeners/Activities/ActivityProvider/UserPilot
/Users/lukas/jiminny/app/app/Jobs/Activity/PushSummaryToCrm
/Users/lukas/jiminny/app/app/Repositories/Crm
/Users/lukas/jiminny/app/app/Jobs/Crm/Delete
/Users/lukas/jiminny/app/app/Services/Kiosk/AutomatedReports
/Users/lukas/jiminny/app/app/Http/Controllers/API/UserAutomatedReports
/Users/lukas/jiminny/app/app/Services/Crm/Salesforce
/Users/lukas/jiminny/app/app/Providers
/Users/lukas/jiminny/app/app/Services/Crm/IntegrationApp
/Users/lukas/jiminny/app/app/Events/Activities/Crm
/Users/lukas/jiminny/app/app/Listeners/Playbooks
/Users/lukas/jiminny/app/app/Console/Commands/Crm
/Users/lukas/jiminny/app/app/Services/Crm
/Users/lukas/jiminny/app/app/Http/Controllers
/Users/lukas/jiminny/app/app/Console/Commands/Reports
/Users/lukas/jiminny/app/app/VO/Repository/OnDemandActivitySearch
/Users/lukas/jiminny/app/app/Listeners/Activities/Conferences/UserPilot
/Users/lukas/jiminny/app/app/Http/Controllers/Webhook
/Users/lukas/jiminny/app/resources/views/emails/reports
/Users/lukas/jiminny/app/app/Mail/Reports
/Users/lukas/jiminny/app/app/Repositories
/Users/lukas/jiminny/app/app/Component/ActivitySearch/Service
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Salesforce
/Users/lukas/jiminny/app/routes
/Users/lukas/jiminny/app/app/Console/Commands
/Users/lukas/jiminny/app/database/migrations
/Users/lukas/Library/Application Support/JetBrains/PhpStorm2026.1/consoles/db/325d461a-c90f-430a-99d4-6ddfce0c61d7
/Users/lukas/jiminny/app/app/Http/Controllers/API/V2
/Users/lukas/jiminny/app/app/Jobs/Crm/Hubspot
/Users/lukas/jiminny/app/app/Component/DealInsights
/Users/lukas/jiminny/app/app/Policies
/Users/lukas/jiminny/app/app/Services/Crm/Helpers
/Users/lukas/jiminny/app/app/Jobs/Crm
/Users/lukas/jiminny/app/app/Models
/Users/lukas/jiminny/app/app/Listeners/Teams
/Users/lukas/jiminny/app/app/Jobs/Crm/Salesforce
/Users/lukas/jiminny/app/app
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/Journal
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/ServiceTraits
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/OpportunitySyncStrategy
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/Webhook
/Users/lukas/jiminny/app/storage/logs
/Users/lukas/jiminny/app
/Users/lukas/jiminny/app/app/Services/Internal
/Users/lukas/jiminny/app/app/Listeners/Transcription
/Users/lukas/jiminny/app/tests/Unit/Listeners/Teams
/Users/lukas/jiminny/app/app/Models/Crm
/Users/lukas/Library/Application Support/JetBrains/PhpStorm2026.1/consoles/db/91133dfa-8d71-4e12-bfb8-fec7f1afba8f
/Users/lukas/jiminny/app/app/Observers
/Users/lukas/jiminny/app/app/Services/Mail
/Users/lukas/jiminny/app/app/Console/Commands/Activities
/Users/lukas/jiminny/app/app/Console/Commands/Activities/Migrator
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot/ServiceTraits
/Users/lukas/jiminny/app/app/Jobs/User
/Users/lukas/jiminny/app/app/Models/Activity
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot/Webhook
/Users/lukas/jiminny/app/app/Component/AiAutomation/Listeners/PendingAnalysis
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot
/Users/lukas/jiminny/app/app/Component/ActivitySearch/FilterDefinition/DealInsights
/Users/lukas/jiminny/app/app/Services/Crm/DecorateActivity
/Users/lukas/jiminny/app/app/Component/Activity/Event
/Users/lukas/jiminny/app/app/Component/Sidekick
/Users/lukas/jiminny/app/app/Listeners/Activities/Conferences
/Users/lukas/jiminny/app/app/Listeners/Activities/Bots
/Users/lukas/jiminny/app/app/Services/RecallAI/Webhooks/Handlers
/Users/lukas/jiminny/app/app/Events/Activities/Bots
/Users/lukas/jiminny/app/app/Component/MeetingBot
/Users/lukas/jiminny/app/app/Services/Activity/RingCentral
/Users/lukas/jiminny/app/app/Http/Controllers/Webhook/Hubspot
/Users/lukas/jiminny/app/app/Services/Activity/Gmail
/Users/lukas/jiminny/app/app/Services/Crm/CrmObjects/ServiceTraits
/Users/lukas/jiminny/app/app/Jobs/Mailbox
/Users/lukas/jiminny/app/app/Console
/Users/lukas/jiminny/app/front-end/src/composables
/Users/lukas/jiminny/app/app/Console/Commands/Calendars
/Users/lukas/jiminny/app/app/Http/Controllers/API
/Users/lukas/jiminny/app/app/Http/Controllers/Internal/WebhookReceiver
/Users/lukas/jiminny/app/app/Services/Crm/IntegrationApp/ServiceTraits
/Users/lukas/jiminny/app/app/Component/Queue
/Users/lukas/jiminny/app/app/Console/Commands/Crm/Hubspot
/Users/lukas/jiminny/app/app/Component/Transcription/Job
/Users/lukas/jiminny/app/tests/Unit/Services/Listeners
/Users/lukas/jiminny/app/app/Services/Crm/Listeners
/Users/lukas/jiminny/app/app/Traits
/Users/lukas/jiminny/app/tests/Unit/Jobs/Crm/Hubspot
/Users/lukas/jiminny/app/tests/Unit/Services/Crm
/Users/lukas/jiminny/app/app/Services/Activity
/Users/lukas/jiminny/app/app/Services/Calendar/Command
/Users/lukas/jiminny/app/.idea/queries
/Users/lukas/jiminny/app/vendor/hubspot/api-client/codegen/Crm
/Users/lukas/jiminny/app/vendor/hubspot
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/Fields
/Users/lukas/jiminny/app/app/Services/Crm/Copper
/Users/lukas/jiminny/app/app/Services/Crm/Bullhorn
/Users/lukas/jiminny/app/app/Notifications/Channels
/Users/lukas/jiminny/app/tests/Unit
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot/Journal
/Users/lukas/jiminny/app/app/Interactions/Settings/Teams
/Users/lukas/jiminny/app/app/Exceptions/Crm
/Users/lukas/jiminny/app/vendor/hubspot/hubspot-php/src/Endpoints
/Users/lukas/jiminny/app/config
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot/OpportunitySyncStrategy
/Users/lukas/jiminny/app/vendor/laravel/framework/src/Illuminate/Redis/Connections
/Users/lukas/jiminny/app/app/Http/Controllers/Settings/Teams
/Users/lukas/jiminny/app/app/Services/Crm/Hubspot/Webhook/Traits
/Users/lukas/jiminny/app/vendor/laravel/framework/src/Illuminate/Broadcasting
/Users/lukas/jiminny/app/app/Component/FeatureFlags
/Users/lukas/jiminny/app/app/Component/Activity
/Users/lukas/jiminny/app/app/Component/ActivitySearch
/Users/lukas/jiminny/app/tests/Unit/Events/Activities/Crm
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/Hubspot/Pagination
/Users/lukas/jiminny/app/app/Console/Commands/Dev
/Users/lukas/jiminny/app/front-end
/Users/lukas/jiminny/app/app/Component/Prophet
/Users/lukas/jiminny/app/tests/Unit/Services/Crm/IntegrationApp
/Users/lukas/jiminny/app/app/Component/AskAnything
/Users/lukas/jiminny/app/app/Component/AskJiminnyAi/OnDemandLevel/Events
/Users/lukas/jiminny/app/app/Component/AskAnything/Events
/Users/lukas/jiminny/app/app/Component/AskJiminnyAi/DealLevel/Traits
/Users/lukas/jiminny/app/app/Component/ProphetAi...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
2850
|
113
|
41
|
2026-05-07T11:43:22.070840+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778154202070_m1.jpg...
|
PhpStorm
|
faVsco.js – Hubspot/Client.php
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
Editor for custom.log
Sync Changes
Hide This Notification
Code changed:
Hide
2
60
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot;
use HubSpot\Client\Crm\Deals\ApiException as DealApiException;
use HubSpot\Client\Crm\Contacts\ApiException as ContactApiException;
use HubSpot\Client\Crm\Companies\ApiException as CompanyApiException;
use HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations as ContactsWithAssociations;
use HubSpot\Client\Crm\Companies\Model\SimplePublicObjectWithAssociations as CompaniesWithAssociations;
use HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations as DealWithAssociations;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectInput;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectWithAssociations as ObjectWithAssociations;
use HubSpot\Client\Crm\Pipelines\Model\Error;
use HubSpot\Client\Crm\Pipelines\Model\PipelineStage;
use HubSpot\Client\Crm\Properties\Model\Property;
use HubSpot\Discovery\Discovery;
use Jiminny\Component\Utility\Service\ProviderRateLimiter;
use Jiminny\Exceptions\CrmException;
use Jiminny\Exceptions\RateLimitException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Jobs\Crm\NoteObject;
use Jiminny\Models\Crm\Field;
use Jiminny\Services\Crm\BaseClient;
use Jiminny\Services\Crm\Hubspot\DTO\Response\Owner;
use Jiminny\Services\SocialAccountService;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use SevenShores\Hubspot\Factory;
use SevenShores\Hubspot\Http\Response;
use Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService;
use Throwable;
/**
* @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
* @param callable(): T $apiCall
* @return T
*
* @throws RateLimitException
*/
private function executeRequest(callable $apiCall)
{
if (! $this->rateLimiter->canMakeRequest($this->config)) {
$retryAfter = $this->rateLimiter->requestAvailableIn($this->config);
$this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
]);
throw new RateLimitException(
'Hubspot rate limit reached for configuration ' . $this->config->getId(),
$retryAfter,
);
}
$this->rateLimiter->incrementRequestCount($this->config);
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function isHubspotRateLimit(Throwable $e): bool
{
return method_exists($e, 'getCode') && (int) $e->getCode() === 429;
}
private function parseRetryAfter(Throwable $e): int
{
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
return 10;
}
public function getMinimumApiVersion(): string
{
return self::MIN_API_VERSION;
}
public function getInstance(): Factory
{
return new Factory([
'key' => $this->accessToken,
'oauth2' => true,
'base_url' => $this->baseUrl,
]);
}
public function getNewInstance(): Discovery
{
return \HubSpot\Factory::createWithAccessToken($this->accessToken);
}
/**
* Secondly and daily limits for Hubspot API
*
* Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)
* Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds
* Daily: 250,000 | 500,000 | 1,000,000
*
* Official documentation states: The search endpoints are rate limited to five requests per second.
* Since with 5 RPS were still hitting secondly rate limits we lowered it to 4
*/
public function getPaginatedData(array $payload, string $type, int $offset = 0): array
{
$total = 0;
$lastId = null;
$rows = [];
foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {
$rows[] = $row;
}
return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
return $this->paginationService->getPaginatedDataGenerator(
$this,
$payload,
$type,
$offset,
$total,
$lastRecordId
);
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
$deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
));
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $batchConfig['api']->read($batchReadRequest);
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
$response = $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
$response = $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
$max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // "110"
$remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // "109"
$interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // "10000"
$body = json_decode((string) $response->getBody(), true);
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));
return $response;
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function createMeeting(array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings';
return $this->makeRequest($endpoint, 'POST', $payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function updateMeeting(string $meetingId, array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings/' . $meetingId;
return $this->makeRequest($endpoint, 'PATCH', $payload);
}
/**
* @throws \Exception
*/
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string {
try {
$noteInput = new SimplePublicObjectInput([
'properties' => [
'hs_note_body' => $body,
'hubspot_owner_id' => $ownerId,
'hs_timestamp' => $timestamp,
],
]);
// Create note
$note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);
$this->getNewInstance()->crm()->objects()->associationsApi()->create(
'note',
$note->getId(),
$this->getNoteObject($noteObject),
$objectId,
$this->getNoteAssociationType($noteObject),
);
return $note->getId();
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to create note', [
'objectId' => $objectId,
'noteObject' => $noteObject->getObjectType(),
'reason' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return null;
}
public function updateEngagement(string $objectId, array $engagement, array $metadata): void
{
$this->getInstance()->engagements()->update($objectId, $engagement, $metadata);
}
public function getEngagementData(string $engagementId): array
{
$engagement = $this->getInstance()->engagements()->get($engagementId);
return $engagement->toArray();
}
public function createEngagement(array $engagement, array $associations, array $metadata): Response
{
return $this->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
}
public function isUnauthorizedException(\Exception $e): bool
{
// Check for specific HubSpot API exception types first
if ($e instanceof BadRequest) {
// BadRequest can contain 401 status codes
return $e->getCode() === 401;
}
// Check for HTTP client exceptions with status codes
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$response = $e->getResponse();
if ($response !== null) {
return $response->getStatusCode() === 401;
}
}
// Check for Guzzle HTTP exceptions
if ($e instanceof \GuzzleHttp\Exception\ClientException) {
return $e->getCode() === 401;
}
// Fallback to string matching as last resort, but be more specific
$message = strtolower($e->getMessage());
return str_contains($message, '401 unauthorized') ||
str_contains($message, 'http 401') ||
str_contains($message, 'status code 401') ||
(preg_match('/\b401\b/', $message) && str_contains($message, 'unauthorized'));
}
/**
* Validates and refreshes the access token if needed before API requests.
* This ensures long-running processes don't fail due to token expiration.
*
* @throws SocialAccountTokenInvalidException
*/
public function ensureValidToken(): void
{
if ($this->oauthAccount === null) {
return;
}
$newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);
if ($newToken !== null) {
$this->accessToken = $newToken;
}
}
public function getConfig()
{
return $this->config;
}
// returns only active (archived=false)
public function getOwners(): array
{
return $this->getNewInstance()->crm()->owners()->getAll();
}
/**
* @param bool $archived
*
* @return array<Owner>|[]
*/
public function getOwnersArchived(bool $archived = true): array
{
$endpoint = '/crm/v3/owners';
$queryParams = [
'archived' => $archived ? 'true' : 'false',
];
$queryString = http_build_query($queryParams);
$owners = [];
try {
$response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);
$responseData = $response?->toArray();
foreach ($responseData['results'] as $result) {
try {
$owners[] = Owner::create($result);
} catch (Throwable $e) {
$this->log->error('[HubSpot] Failed to process owner data', [
'result' => $result,
'error' => $e->getMessage(),
]);
continue;
}
}
} catch (Throwable $e) {
$this->log->error('HubSpot] Failed to fetch owners', [
'archived' => $archived,
'error' => $e->getMessage(),
]);
return [];
}
return $owners;
}
public function getMeeting(string $engagementId): ObjectWithAssociations
{
return $this->getNewInstance()->crm()->objects()->basicApi()
->getById('meeting', $engagementId, null, 'contact,company,deal');
}
public function deleteEngagement(string $engagementId): void
{
$this->getInstance()->engagements()->delete((int) $engagementId);
}
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array
{
$associationData = [];
$idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);
foreach ($idChunks as $idChunk) {
try {
$batchInput = new \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId();
$batchInput->setInputs(array_map(function ($id) {
$publicObjectId = new \HubSpot\Client\Crm\Associations\Model\PublicObjectId();
$publicObjectId->setId($id);
return $publicObjectId;
}, $idChunk));
$associatedObjectsData = $this
->getNewInstance()
->crm()
->associations()
->batchApi()
->read($fromObject, $toObject, $batchInput);
if ($associatedObjectsData instanceof \HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti) {
foreach ($associatedObjectsData->getResults() as $association) {
$from = $association->getFrom()->getId();
$toAssociations = $association->getTo();
if (! empty($toAssociations)) {
$associationData[$from] = array_map(function ($item) {
return $item->getId();
}, $toAssociations);
}
}
}
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to fetch associations', [
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => $e->getMessage(),
]);
}
}
return $associationData;
}
/**
* @throws \Exception
*/
private function getNoteAssociationType(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'note_to_deal',
NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it
NoteObject::Account => 'note_to_company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
/**
* @throws \Exception
*/
private function getNoteObject(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'deal',
NoteObject::Lead, NoteObject::Contact => 'contact',
NoteObject::Account => 'company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
public function addAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/create";
return $this->makeRequest($endpoint, 'POST', $payload);
}
public function removeAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/archive";
return $this->makeRequest($endpoint, 'POST', $payload);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"on_screen":true,"help_text":"Git Branch: master","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"Editor for custom.log","depth":4,"on_screen":true,"role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.016666668,"height":0.02111111},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"60","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.021527778,"height":0.02111111},"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.015277778,"height":0.025555555},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.014583333,"height":0.025555555},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Component\\Utility\\Service\\ProviderRateLimiter;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n private ProviderRateLimiter $rateLimiter;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager,\n ProviderRateLimiter $rateLimiter,\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n $this->rateLimiter = $rateLimiter;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Single entry point for every HubSpot API call. Enforces the per-portal\n * rate limit configured in the rate_limits table (morphed to the current\n * Configuration) and reacts to a real 429 from HubSpot by translating it\n * into a RateLimitException carrying Retry-After.\n *\n * Wrap any outbound HubSpot call (SDK or raw HTTP) like:\n *\n * $this->executeRequest(fn () => $this->getNewInstance()->crm()->...);\n *\n * @template T\n * @param callable(): T $apiCall\n * @return T\n *\n * @throws RateLimitException\n */\n private function executeRequest(callable $apiCall)\n {\n if (! $this->rateLimiter->canMakeRequest($this->config)) {\n $retryAfter = $this->rateLimiter->requestAvailableIn($this->config);\n\n $this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n ]);\n\n throw new RateLimitException(\n 'Hubspot rate limit reached for configuration ' . $this->config->getId(),\n $retryAfter,\n );\n }\n\n $this->rateLimiter->incrementRequestCount($this->config);\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n private function isHubspotRateLimit(Throwable $e): bool\n {\n return method_exists($e, 'getCode') && (int) $e->getCode() === 429;\n }\n\n private function parseRetryAfter(Throwable $e): int\n {\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n return 10;\n }\n\n public function getMinimumApiVersion(): string\n {\n return self::MIN_API_VERSION;\n }\n\n public function getInstance(): Factory\n {\n return new Factory([\n 'key' => $this->accessToken,\n 'oauth2' => true,\n 'base_url' => $this->baseUrl,\n ]);\n }\n\n public function getNewInstance(): Discovery\n {\n return \\HubSpot\\Factory::createWithAccessToken($this->accessToken);\n }\n\n /**\n * Secondly and daily limits for Hubspot API\n *\n * Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)\n * Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds\n * Daily: 250,000 | 500,000 | 1,000,000\n *\n * Official documentation states: The search endpoints are rate limited to five requests per second.\n * Since with 5 RPS were still hitting secondly rate limits we lowered it to 4\n */\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array\n {\n $total = 0;\n $lastId = null;\n $rows = [];\n foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {\n $rows[] = $row;\n }\n\n return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n return $this->paginationService->getPaginatedDataGenerator(\n $this,\n $payload,\n $type,\n $offset,\n $total,\n $lastRecordId\n );\n }\n\n /**\n * @throws DealApiException\n * @throws CrmException\n */\n public function getOpportunityById(string $crmId, array $fields): array\n {\n try {\n $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n ));\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n $response = $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n $response = $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n\n $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // \"110\"\n $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // \"109\"\n $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // \"10000\"\n $body = json_decode((string) $response->getBody(), true);\n\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));\n\n return $response;\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Component\\Utility\\Service\\ProviderRateLimiter;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n private ProviderRateLimiter $rateLimiter;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager,\n ProviderRateLimiter $rateLimiter,\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n $this->rateLimiter = $rateLimiter;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Single entry point for every HubSpot API call. Enforces the per-portal\n * rate limit configured in the rate_limits table (morphed to the current\n * Configuration) and reacts to a real 429 from HubSpot by translating it\n * into a RateLimitException carrying Retry-After.\n *\n * Wrap any outbound HubSpot call (SDK or raw HTTP) like:\n *\n * $this->executeRequest(fn () => $this->getNewInstance()->crm()->...);\n *\n * @template T\n * @param callable(): T $apiCall\n * @return T\n *\n * @throws RateLimitException\n */\n private function executeRequest(callable $apiCall)\n {\n if (! $this->rateLimiter->canMakeRequest($this->config)) {\n $retryAfter = $this->rateLimiter->requestAvailableIn($this->config);\n\n $this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n ]);\n\n throw new RateLimitException(\n 'Hubspot rate limit reached for configuration ' . $this->config->getId(),\n $retryAfter,\n );\n }\n\n $this->rateLimiter->incrementRequestCount($this->config);\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n private function isHubspotRateLimit(Throwable $e): bool\n {\n return method_exists($e, 'getCode') && (int) $e->getCode() === 429;\n }\n\n private function parseRetryAfter(Throwable $e): int\n {\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n return 10;\n }\n\n public function getMinimumApiVersion(): string\n {\n return self::MIN_API_VERSION;\n }\n\n public function getInstance(): Factory\n {\n return new Factory([\n 'key' => $this->accessToken,\n 'oauth2' => true,\n 'base_url' => $this->baseUrl,\n ]);\n }\n\n public function getNewInstance(): Discovery\n {\n return \\HubSpot\\Factory::createWithAccessToken($this->accessToken);\n }\n\n /**\n * Secondly and daily limits for Hubspot API\n *\n * Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)\n * Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds\n * Daily: 250,000 | 500,000 | 1,000,000\n *\n * Official documentation states: The search endpoints are rate limited to five requests per second.\n * Since with 5 RPS were still hitting secondly rate limits we lowered it to 4\n */\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array\n {\n $total = 0;\n $lastId = null;\n $rows = [];\n foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {\n $rows[] = $row;\n }\n\n return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n return $this->paginationService->getPaginatedDataGenerator(\n $this,\n $payload,\n $type,\n $offset,\n $total,\n $lastRecordId\n );\n }\n\n /**\n * @throws DealApiException\n * @throws CrmException\n */\n public function getOpportunityById(string $crmId, array $fields): array\n {\n try {\n $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n ));\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n $response = $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n $response = $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n\n $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // \"110\"\n $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // \"109\"\n $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // \"10000\"\n $body = json_decode((string) $response->getBody(), true);\n\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));\n\n return $response;\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-188086479456983350
|
6378757184196315236
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
Editor for custom.log
Sync Changes
Hide This Notification
Code changed:
Hide
2
60
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot;
use HubSpot\Client\Crm\Deals\ApiException as DealApiException;
use HubSpot\Client\Crm\Contacts\ApiException as ContactApiException;
use HubSpot\Client\Crm\Companies\ApiException as CompanyApiException;
use HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations as ContactsWithAssociations;
use HubSpot\Client\Crm\Companies\Model\SimplePublicObjectWithAssociations as CompaniesWithAssociations;
use HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations as DealWithAssociations;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectInput;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectWithAssociations as ObjectWithAssociations;
use HubSpot\Client\Crm\Pipelines\Model\Error;
use HubSpot\Client\Crm\Pipelines\Model\PipelineStage;
use HubSpot\Client\Crm\Properties\Model\Property;
use HubSpot\Discovery\Discovery;
use Jiminny\Component\Utility\Service\ProviderRateLimiter;
use Jiminny\Exceptions\CrmException;
use Jiminny\Exceptions\RateLimitException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Jobs\Crm\NoteObject;
use Jiminny\Models\Crm\Field;
use Jiminny\Services\Crm\BaseClient;
use Jiminny\Services\Crm\Hubspot\DTO\Response\Owner;
use Jiminny\Services\SocialAccountService;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use SevenShores\Hubspot\Factory;
use SevenShores\Hubspot\Http\Response;
use Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService;
use Throwable;
/**
* @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
* @param callable(): T $apiCall
* @return T
*
* @throws RateLimitException
*/
private function executeRequest(callable $apiCall)
{
if (! $this->rateLimiter->canMakeRequest($this->config)) {
$retryAfter = $this->rateLimiter->requestAvailableIn($this->config);
$this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
]);
throw new RateLimitException(
'Hubspot rate limit reached for configuration ' . $this->config->getId(),
$retryAfter,
);
}
$this->rateLimiter->incrementRequestCount($this->config);
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function isHubspotRateLimit(Throwable $e): bool
{
return method_exists($e, 'getCode') && (int) $e->getCode() === 429;
}
private function parseRetryAfter(Throwable $e): int
{
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
return 10;
}
public function getMinimumApiVersion(): string
{
return self::MIN_API_VERSION;
}
public function getInstance(): Factory
{
return new Factory([
'key' => $this->accessToken,
'oauth2' => true,
'base_url' => $this->baseUrl,
]);
}
public function getNewInstance(): Discovery
{
return \HubSpot\Factory::createWithAccessToken($this->accessToken);
}
/**
* Secondly and daily limits for Hubspot API
*
* Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)
* Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds
* Daily: 250,000 | 500,000 | 1,000,000
*
* Official documentation states: The search endpoints are rate limited to five requests per second.
* Since with 5 RPS were still hitting secondly rate limits we lowered it to 4
*/
public function getPaginatedData(array $payload, string $type, int $offset = 0): array
{
$total = 0;
$lastId = null;
$rows = [];
foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {
$rows[] = $row;
}
return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
return $this->paginationService->getPaginatedDataGenerator(
$this,
$payload,
$type,
$offset,
$total,
$lastRecordId
);
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
$deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
));
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $batchConfig['api']->read($batchReadRequest);
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
$response = $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
$response = $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
$max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // "110"
$remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // "109"
$interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // "10000"
$body = json_decode((string) $response->getBody(), true);
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));
return $response;
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function createMeeting(array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings';
return $this->makeRequest($endpoint, 'POST', $payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function updateMeeting(string $meetingId, array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings/' . $meetingId;
return $this->makeRequest($endpoint, 'PATCH', $payload);
}
/**
* @throws \Exception
*/
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string {
try {
$noteInput = new SimplePublicObjectInput([
'properties' => [
'hs_note_body' => $body,
'hubspot_owner_id' => $ownerId,
'hs_timestamp' => $timestamp,
],
]);
// Create note
$note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);
$this->getNewInstance()->crm()->objects()->associationsApi()->create(
'note',
$note->getId(),
$this->getNoteObject($noteObject),
$objectId,
$this->getNoteAssociationType($noteObject),
);
return $note->getId();
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to create note', [
'objectId' => $objectId,
'noteObject' => $noteObject->getObjectType(),
'reason' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return null;
}
public function updateEngagement(string $objectId, array $engagement, array $metadata): void
{
$this->getInstance()->engagements()->update($objectId, $engagement, $metadata);
}
public function getEngagementData(string $engagementId): array
{
$engagement = $this->getInstance()->engagements()->get($engagementId);
return $engagement->toArray();
}
public function createEngagement(array $engagement, array $associations, array $metadata): Response
{
return $this->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
}
public function isUnauthorizedException(\Exception $e): bool
{
// Check for specific HubSpot API exception types first
if ($e instanceof BadRequest) {
// BadRequest can contain 401 status codes
return $e->getCode() === 401;
}
// Check for HTTP client exceptions with status codes
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$response = $e->getResponse();
if ($response !== null) {
return $response->getStatusCode() === 401;
}
}
// Check for Guzzle HTTP exceptions
if ($e instanceof \GuzzleHttp\Exception\ClientException) {
return $e->getCode() === 401;
}
// Fallback to string matching as last resort, but be more specific
$message = strtolower($e->getMessage());
return str_contains($message, '401 unauthorized') ||
str_contains($message, 'http 401') ||
str_contains($message, 'status code 401') ||
(preg_match('/\b401\b/', $message) && str_contains($message, 'unauthorized'));
}
/**
* Validates and refreshes the access token if needed before API requests.
* This ensures long-running processes don't fail due to token expiration.
*
* @throws SocialAccountTokenInvalidException
*/
public function ensureValidToken(): void
{
if ($this->oauthAccount === null) {
return;
}
$newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);
if ($newToken !== null) {
$this->accessToken = $newToken;
}
}
public function getConfig()
{
return $this->config;
}
// returns only active (archived=false)
public function getOwners(): array
{
return $this->getNewInstance()->crm()->owners()->getAll();
}
/**
* @param bool $archived
*
* @return array<Owner>|[]
*/
public function getOwnersArchived(bool $archived = true): array
{
$endpoint = '/crm/v3/owners';
$queryParams = [
'archived' => $archived ? 'true' : 'false',
];
$queryString = http_build_query($queryParams);
$owners = [];
try {
$response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);
$responseData = $response?->toArray();
foreach ($responseData['results'] as $result) {
try {
$owners[] = Owner::create($result);
} catch (Throwable $e) {
$this->log->error('[HubSpot] Failed to process owner data', [
'result' => $result,
'error' => $e->getMessage(),
]);
continue;
}
}
} catch (Throwable $e) {
$this->log->error('HubSpot] Failed to fetch owners', [
'archived' => $archived,
'error' => $e->getMessage(),
]);
return [];
}
return $owners;
}
public function getMeeting(string $engagementId): ObjectWithAssociations
{
return $this->getNewInstance()->crm()->objects()->basicApi()
->getById('meeting', $engagementId, null, 'contact,company,deal');
}
public function deleteEngagement(string $engagementId): void
{
$this->getInstance()->engagements()->delete((int) $engagementId);
}
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array
{
$associationData = [];
$idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);
foreach ($idChunks as $idChunk) {
try {
$batchInput = new \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId();
$batchInput->setInputs(array_map(function ($id) {
$publicObjectId = new \HubSpot\Client\Crm\Associations\Model\PublicObjectId();
$publicObjectId->setId($id);
return $publicObjectId;
}, $idChunk));
$associatedObjectsData = $this
->getNewInstance()
->crm()
->associations()
->batchApi()
->read($fromObject, $toObject, $batchInput);
if ($associatedObjectsData instanceof \HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti) {
foreach ($associatedObjectsData->getResults() as $association) {
$from = $association->getFrom()->getId();
$toAssociations = $association->getTo();
if (! empty($toAssociations)) {
$associationData[$from] = array_map(function ($item) {
return $item->getId();
}, $toAssociations);
}
}
}
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to fetch associations', [
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => $e->getMessage(),
]);
}
}
return $associationData;
}
/**
* @throws \Exception
*/
private function getNoteAssociationType(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'note_to_deal',
NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it
NoteObject::Account => 'note_to_company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
/**
* @throws \Exception
*/
private function getNoteObject(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'deal',
NoteObject::Lead, NoteObject::Contact => 'contact',
NoteObject::Account => 'company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
public function addAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/create";
return $this->makeRequest($endpoint, 'POST', $payload);
}
public function removeAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/archive";
return $this->makeRequest($endpoint, 'POST', $payload);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
2845
|
NULL
|
NULL
|
NULL
|
|
2849
|
114
|
40
|
2026-05-07T11:43:21.921884+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778154201921_m2.jpg...
|
PhpStorm
|
faVsco.js – Hubspot/Client.php
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
Editor for custom.log
Sync Changes
Hide This Notification
Code changed:
Hide
2
60
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot;
use HubSpot\Client\Crm\Deals\ApiException as DealApiException;
use HubSpot\Client\Crm\Contacts\ApiException as ContactApiException;
use HubSpot\Client\Crm\Companies\ApiException as CompanyApiException;
use HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations as ContactsWithAssociations;
use HubSpot\Client\Crm\Companies\Model\SimplePublicObjectWithAssociations as CompaniesWithAssociations;
use HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations as DealWithAssociations;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectInput;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectWithAssociations as ObjectWithAssociations;
use HubSpot\Client\Crm\Pipelines\Model\Error;
use HubSpot\Client\Crm\Pipelines\Model\PipelineStage;
use HubSpot\Client\Crm\Properties\Model\Property;
use HubSpot\Discovery\Discovery;
use Jiminny\Component\Utility\Service\ProviderRateLimiter;
use Jiminny\Exceptions\CrmException;
use Jiminny\Exceptions\RateLimitException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Jobs\Crm\NoteObject;
use Jiminny\Models\Crm\Field;
use Jiminny\Services\Crm\BaseClient;
use Jiminny\Services\Crm\Hubspot\DTO\Response\Owner;
use Jiminny\Services\SocialAccountService;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use SevenShores\Hubspot\Factory;
use SevenShores\Hubspot\Http\Response;
use Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService;
use Throwable;
/**
* @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
* @param callable(): T $apiCall
* @return T
*
* @throws RateLimitException
*/
private function executeRequest(callable $apiCall)
{
if (! $this->rateLimiter->canMakeRequest($this->config)) {
$retryAfter = $this->rateLimiter->requestAvailableIn($this->config);
$this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
]);
throw new RateLimitException(
'Hubspot rate limit reached for configuration ' . $this->config->getId(),
$retryAfter,
);
}
$this->rateLimiter->incrementRequestCount($this->config);
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function isHubspotRateLimit(Throwable $e): bool
{
return method_exists($e, 'getCode') && (int) $e->getCode() === 429;
}
private function parseRetryAfter(Throwable $e): int
{
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
return 10;
}
public function getMinimumApiVersion(): string
{
return self::MIN_API_VERSION;
}
public function getInstance(): Factory
{
return new Factory([
'key' => $this->accessToken,
'oauth2' => true,
'base_url' => $this->baseUrl,
]);
}
public function getNewInstance(): Discovery
{
return \HubSpot\Factory::createWithAccessToken($this->accessToken);
}
/**
* Secondly and daily limits for Hubspot API
*
* Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)
* Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds
* Daily: 250,000 | 500,000 | 1,000,000
*
* Official documentation states: The search endpoints are rate limited to five requests per second.
* Since with 5 RPS were still hitting secondly rate limits we lowered it to 4
*/
public function getPaginatedData(array $payload, string $type, int $offset = 0): array
{
$total = 0;
$lastId = null;
$rows = [];
foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {
$rows[] = $row;
}
return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
return $this->paginationService->getPaginatedDataGenerator(
$this,
$payload,
$type,
$offset,
$total,
$lastRecordId
);
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
$deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
));
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $batchConfig['api']->read($batchReadRequest);
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
$response = $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
$response = $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
$max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // "110"
$remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // "109"
$interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // "10000"
$body = json_decode((string) $response->getBody(), true);
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));
return $response;
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function createMeeting(array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings';
return $this->makeRequest($endpoint, 'POST', $payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function updateMeeting(string $meetingId, array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings/' . $meetingId;
return $this->makeRequest($endpoint, 'PATCH', $payload);
}
/**
* @throws \Exception
*/
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string {
try {
$noteInput = new SimplePublicObjectInput([
'properties' => [
'hs_note_body' => $body,
'hubspot_owner_id' => $ownerId,
'hs_timestamp' => $timestamp,
],
]);
// Create note
$note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);
$this->getNewInstance()->crm()->objects()->associationsApi()->create(
'note',
$note->getId(),
$this->getNoteObject($noteObject),
$objectId,
$this->getNoteAssociationType($noteObject),
);
return $note->getId();
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to create note', [
'objectId' => $objectId,
'noteObject' => $noteObject->getObjectType(),
'reason' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return null;
}
public function updateEngagement(string $objectId, array $engagement, array $metadata): void
{
$this->getInstance()->engagements()->update($objectId, $engagement, $metadata);
}
public function getEngagementData(string $engagementId): array
{
$engagement = $this->getInstance()->engagements()->get($engagementId);
return $engagement->toArray();
}
public function createEngagement(array $engagement, array $associations, array $metadata): Response
{
return $this->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
}
public function isUnauthorizedException(\Exception $e): bool
{
// Check for specific HubSpot API exception types first
if ($e instanceof BadRequest) {
// BadRequest can contain 401 status codes
return $e->getCode() === 401;
}
// Check for HTTP client exceptions with status codes
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$response = $e->getResponse();
if ($response !== null) {
return $response->getStatusCode() === 401;
}
}
// Check for Guzzle HTTP exceptions
if ($e instanceof \GuzzleHttp\Exception\ClientException) {
return $e->getCode() === 401;
}
// Fallback to string matching as last resort, but be more specific
$message = strtolower($e->getMessage());
return str_contains($message, '401 unauthorized') ||
str_contains($message, 'http 401') ||
str_contains($message, 'status code 401') ||
(preg_match('/\b401\b/', $message) && str_contains($message, 'unauthorized'));
}
/**
* Validates and refreshes the access token if needed before API requests.
* This ensures long-running processes don't fail due to token expiration.
*
* @throws SocialAccountTokenInvalidException
*/
public function ensureValidToken(): void
{
if ($this->oauthAccount === null) {
return;
}
$newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);
if ($newToken !== null) {
$this->accessToken = $newToken;
}
}
public function getConfig()
{
return $this->config;
}
// returns only active (archived=false)
public function getOwners(): array
{
return $this->getNewInstance()->crm()->owners()->getAll();
}
/**
* @param bool $archived
*
* @return array<Owner>|[]
*/
public function getOwnersArchived(bool $archived = true): array
{
$endpoint = '/crm/v3/owners';
$queryParams = [
'archived' => $archived ? 'true' : 'false',
];
$queryString = http_build_query($queryParams);
$owners = [];
try {
$response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);
$responseData = $response?->toArray();
foreach ($responseData['results'] as $result) {
try {
$owners[] = Owner::create($result);
} catch (Throwable $e) {
$this->log->error('[HubSpot] Failed to process owner data', [
'result' => $result,
'error' => $e->getMessage(),
]);
continue;
}
}
} catch (Throwable $e) {
$this->log->error('HubSpot] Failed to fetch owners', [
'archived' => $archived,
'error' => $e->getMessage(),
]);
return [];
}
return $owners;
}
public function getMeeting(string $engagementId): ObjectWithAssociations
{
return $this->getNewInstance()->crm()->objects()->basicApi()
->getById('meeting', $engagementId, null, 'contact,company,deal');
}
public function deleteEngagement(string $engagementId): void
{
$this->getInstance()->engagements()->delete((int) $engagementId);
}
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array
{
$associationData = [];
$idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);
foreach ($idChunks as $idChunk) {
try {
$batchInput = new \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId();
$batchInput->setInputs(array_map(function ($id) {
$publicObjectId = new \HubSpot\Client\Crm\Associations\Model\PublicObjectId();
$publicObjectId->setId($id);
return $publicObjectId;
}, $idChunk));
$associatedObjectsData = $this
->getNewInstance()
->crm()
->associations()
->batchApi()
->read($fromObject, $toObject, $batchInput);
if ($associatedObjectsData instanceof \HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti) {
foreach ($associatedObjectsData->getResults() as $association) {
$from = $association->getFrom()->getId();
$toAssociations = $association->getTo();
if (! empty($toAssociations)) {
$associationData[$from] = array_map(function ($item) {
return $item->getId();
}, $toAssociations);
}
}
}
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to fetch associations', [
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => $e->getMessage(),
]);
}
}
return $associationData;
}
/**
* @throws \Exception
*/
private function getNoteAssociationType(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'note_to_deal',
NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it
NoteObject::Account => 'note_to_company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
/**
* @throws \Exception
*/
private function getNoteObject(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'deal',
NoteObject::Lead, NoteObject::Contact => 'contact',
NoteObject::Account => 'company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
public function addAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/create";
return $this->makeRequest($endpoint, 'POST', $payload);
}
public function removeAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/archive";
return $this->makeRequest($endpoint, 'POST', $payload);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.034242023,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: master","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"Editor for custom.log","depth":4,"bounds":{"left":0.5475399,"top":0.0726257,"width":0.44082448,"height":0.9066241},"on_screen":true,"role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.007978723,"height":0.0},"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"60","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.010305851,"height":0.0},"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.00731383,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.006981383,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Component\\Utility\\Service\\ProviderRateLimiter;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n private ProviderRateLimiter $rateLimiter;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager,\n ProviderRateLimiter $rateLimiter,\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n $this->rateLimiter = $rateLimiter;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Single entry point for every HubSpot API call. Enforces the per-portal\n * rate limit configured in the rate_limits table (morphed to the current\n * Configuration) and reacts to a real 429 from HubSpot by translating it\n * into a RateLimitException carrying Retry-After.\n *\n * Wrap any outbound HubSpot call (SDK or raw HTTP) like:\n *\n * $this->executeRequest(fn () => $this->getNewInstance()->crm()->...);\n *\n * @template T\n * @param callable(): T $apiCall\n * @return T\n *\n * @throws RateLimitException\n */\n private function executeRequest(callable $apiCall)\n {\n if (! $this->rateLimiter->canMakeRequest($this->config)) {\n $retryAfter = $this->rateLimiter->requestAvailableIn($this->config);\n\n $this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n ]);\n\n throw new RateLimitException(\n 'Hubspot rate limit reached for configuration ' . $this->config->getId(),\n $retryAfter,\n );\n }\n\n $this->rateLimiter->incrementRequestCount($this->config);\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n private function isHubspotRateLimit(Throwable $e): bool\n {\n return method_exists($e, 'getCode') && (int) $e->getCode() === 429;\n }\n\n private function parseRetryAfter(Throwable $e): int\n {\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n return 10;\n }\n\n public function getMinimumApiVersion(): string\n {\n return self::MIN_API_VERSION;\n }\n\n public function getInstance(): Factory\n {\n return new Factory([\n 'key' => $this->accessToken,\n 'oauth2' => true,\n 'base_url' => $this->baseUrl,\n ]);\n }\n\n public function getNewInstance(): Discovery\n {\n return \\HubSpot\\Factory::createWithAccessToken($this->accessToken);\n }\n\n /**\n * Secondly and daily limits for Hubspot API\n *\n * Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)\n * Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds\n * Daily: 250,000 | 500,000 | 1,000,000\n *\n * Official documentation states: The search endpoints are rate limited to five requests per second.\n * Since with 5 RPS were still hitting secondly rate limits we lowered it to 4\n */\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array\n {\n $total = 0;\n $lastId = null;\n $rows = [];\n foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {\n $rows[] = $row;\n }\n\n return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n return $this->paginationService->getPaginatedDataGenerator(\n $this,\n $payload,\n $type,\n $offset,\n $total,\n $lastRecordId\n );\n }\n\n /**\n * @throws DealApiException\n * @throws CrmException\n */\n public function getOpportunityById(string $crmId, array $fields): array\n {\n try {\n $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n ));\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n $response = $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n $response = $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n\n $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // \"110\"\n $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // \"109\"\n $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // \"10000\"\n $body = json_decode((string) $response->getBody(), true);\n\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));\n\n return $response;\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse HubSpot\\Client\\Crm\\Deals\\ApiException as DealApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\ApiException as ContactApiException;\nuse HubSpot\\Client\\Crm\\Companies\\ApiException as CompanyApiException;\nuse HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectWithAssociations as ContactsWithAssociations;\nuse HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectWithAssociations as CompaniesWithAssociations;\nuse HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectWithAssociations as DealWithAssociations;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectInput;\nuse HubSpot\\Client\\Crm\\Objects\\Model\\SimplePublicObjectWithAssociations as ObjectWithAssociations;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\Error;\nuse HubSpot\\Client\\Crm\\Pipelines\\Model\\PipelineStage;\nuse HubSpot\\Client\\Crm\\Properties\\Model\\Property;\nuse HubSpot\\Discovery\\Discovery;\nuse Jiminny\\Component\\Utility\\Service\\ProviderRateLimiter;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Services\\Crm\\BaseClient;\nuse Jiminny\\Services\\Crm\\Hubspot\\DTO\\Response\\Owner;\nuse Jiminny\\Services\\SocialAccountService;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse SevenShores\\Hubspot\\Factory;\nuse SevenShores\\Hubspot\\Http\\Response;\nuse Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService;\nuse Throwable;\n\n/**\n * @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}\n */\nclass Client extends BaseClient implements HubspotClientInterface\n{\n public const string MIN_API_VERSION = '2';\n\n public const string BASE_URL = 'https://api.hubapi.com';\n\n public const int ASSOCIATIONS_BATCH_SIZE_LIMIT = 1000;\n\n private HubspotPaginationService $paginationService;\n private HubspotTokenManager $tokenManager;\n private ProviderRateLimiter $rateLimiter;\n\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager,\n ProviderRateLimiter $rateLimiter,\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n $this->rateLimiter = $rateLimiter;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Single entry point for every HubSpot API call. Enforces the per-portal\n * rate limit configured in the rate_limits table (morphed to the current\n * Configuration) and reacts to a real 429 from HubSpot by translating it\n * into a RateLimitException carrying Retry-After.\n *\n * Wrap any outbound HubSpot call (SDK or raw HTTP) like:\n *\n * $this->executeRequest(fn () => $this->getNewInstance()->crm()->...);\n *\n * @template T\n * @param callable(): T $apiCall\n * @return T\n *\n * @throws RateLimitException\n */\n private function executeRequest(callable $apiCall)\n {\n if (! $this->rateLimiter->canMakeRequest($this->config)) {\n $retryAfter = $this->rateLimiter->requestAvailableIn($this->config);\n\n $this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n ]);\n\n throw new RateLimitException(\n 'Hubspot rate limit reached for configuration ' . $this->config->getId(),\n $retryAfter,\n );\n }\n\n $this->rateLimiter->incrementRequestCount($this->config);\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n $this->log->warning('[Hubspot] Received 429 from API', [\n 'team_id' => $this->config->team_id,\n 'config_id' => $this->config->getId(),\n 'retry_after' => $retryAfter,\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n }\n\n throw $e;\n }\n }\n\n private function isHubspotRateLimit(Throwable $e): bool\n {\n return method_exists($e, 'getCode') && (int) $e->getCode() === 429;\n }\n\n private function parseRetryAfter(Throwable $e): int\n {\n if (method_exists($e, 'getResponseHeaders')) {\n $headers = $e->getResponseHeaders() ?: [];\n $value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;\n if (is_array($value)) {\n $value = $value[0] ?? null;\n }\n if (is_numeric($value)) {\n return (int) $value;\n }\n }\n\n return 10;\n }\n\n public function getMinimumApiVersion(): string\n {\n return self::MIN_API_VERSION;\n }\n\n public function getInstance(): Factory\n {\n return new Factory([\n 'key' => $this->accessToken,\n 'oauth2' => true,\n 'base_url' => $this->baseUrl,\n ]);\n }\n\n public function getNewInstance(): Discovery\n {\n return \\HubSpot\\Factory::createWithAccessToken($this->accessToken);\n }\n\n /**\n * Secondly and daily limits for Hubspot API\n *\n * Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)\n * Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds\n * Daily: 250,000 | 500,000 | 1,000,000\n *\n * Official documentation states: The search endpoints are rate limited to five requests per second.\n * Since with 5 RPS were still hitting secondly rate limits we lowered it to 4\n */\n public function getPaginatedData(array $payload, string $type, int $offset = 0): array\n {\n $total = 0;\n $lastId = null;\n $rows = [];\n foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {\n $rows[] = $row;\n }\n\n return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n return $this->paginationService->getPaginatedDataGenerator(\n $this,\n $payload,\n $type,\n $offset,\n $total,\n $lastRecordId\n );\n }\n\n /**\n * @throws DealApiException\n * @throws CrmException\n */\n public function getOpportunityById(string $crmId, array $fields): array\n {\n try {\n $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n 'companies,contacts'\n ));\n } catch (DealApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $deal instanceof DealWithAssociations) {\n throw new CrmException('Deal not found');\n }\n\n return [\n 'id' => $deal->getId(),\n 'properties' => $deal->getProperties(),\n 'associations' => $deal->getAssociations(),\n ];\n }\n\n /**\n * Generic batch read method for HubSpot objects\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts')\n * @param array<string> $crmIds Array of HubSpot object IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with object data\n */\n private function batchReadObjects(string $objectType, array $crmIds, array $fields): array\n {\n if (empty($crmIds)) {\n return [];\n }\n\n $this->validateBatchSize($objectType, $crmIds);\n $this->ensureValidToken();\n\n try {\n $batchConfig = $this->createBatchConfiguration($objectType);\n $batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);\n $response = $batchConfig['api']->read($batchReadRequest);\n\n $this->validateApiResponse($response, $objectType);\n\n $results = $this->processApiResults($response);\n $this->logBatchResults($objectType, $crmIds, $results);\n\n return $results;\n } catch (\\Throwable $e) {\n $this->handleBatchError($e, $objectType, $crmIds);\n }\n }\n\n private function validateBatchSize(string $objectType, array $crmIds): void\n {\n if (count($crmIds) > 100) {\n throw new \\InvalidArgumentException(\"Batch size cannot exceed 100 {$objectType}\");\n }\n }\n\n private function createBatchConfiguration(string $objectType): array\n {\n $configurations = [\n 'deals' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Deals\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Deals\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->deals()->batchApi(),\n ],\n 'companies' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Companies\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Companies\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->companies()->batchApi(),\n ],\n 'contacts' => [\n 'batchReadRequest' => new \\HubSpot\\Client\\Crm\\Contacts\\Model\\BatchReadInputSimplePublicObjectId(),\n 'inputClass' => \\HubSpot\\Client\\Crm\\Contacts\\Model\\SimplePublicObjectId::class,\n 'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),\n ],\n ];\n\n if (! isset($configurations[$objectType])) {\n throw new \\InvalidArgumentException(\"Unsupported object type: {$objectType}\");\n }\n\n return $configurations[$objectType];\n }\n\n private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object\n {\n $batchReadRequest = $batchConfig['batchReadRequest'];\n $inputClass = $batchConfig['inputClass'];\n\n $inputs = array_map(function ($crmId) use ($inputClass) {\n $input = new $inputClass();\n $input->setId($crmId);\n\n return $input;\n }, $crmIds);\n\n $batchReadRequest->setInputs($inputs);\n $batchReadRequest->setProperties($fields);\n\n return $batchReadRequest;\n }\n\n private function validateApiResponse($response, string $objectType): void\n {\n if (! $response) {\n throw new CrmException(\"HubSpot API returned null response for {$objectType} batch read\");\n }\n }\n\n private function processApiResults($response): array\n {\n $results = [];\n $responseResults = $response->getResults();\n\n if ($responseResults) {\n foreach ($responseResults as $object) {\n if ($object && $object->getId()) {\n $results[$object->getId()] = [\n 'id' => $object->getId(),\n 'properties' => $object->getProperties() ?: [],\n ];\n }\n }\n }\n\n return $results;\n }\n\n private function logBatchResults(string $objectType, array $crmIds, array $results): void\n {\n $this->log->info(\"[HubSpot] Batch fetched {$objectType}\", [\n 'requested_count' => count($crmIds),\n 'returned_count' => count($results),\n 'crm_ids' => $crmIds,\n ]);\n }\n\n private function handleBatchError(\\Throwable $e, string $objectType, array $crmIds): void\n {\n $errorMessage = $e->getMessage() ?: 'Unknown error';\n $errorTrace = $e->getTraceAsString() ?: 'No trace available';\n\n $this->log->error(\"[HubSpot] Failed to batch fetch {$objectType}\", [\n 'crm_ids' => $crmIds,\n 'error' => $errorMessage,\n 'trace' => $errorTrace,\n ]);\n\n throw new CrmException(\"Failed to batch fetch {$objectType}: \" . $errorMessage);\n }\n\n /**\n * Batch read multiple opportunities by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot deal IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with opportunity data\n */\n public function getOpportunitiesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('deals', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple companies by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot company IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with company data\n */\n public function getCompaniesByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('companies', $crmIds, $fields);\n }\n\n /**\n * Batch read multiple contacts by their CRM IDs\n *\n * @param array<string> $crmIds Array of HubSpot contact IDs (max 100)\n * @param array<string> $fields Array of property names to fetch\n *\n * @return array<string, array> Array keyed by CRM ID with contact data\n */\n public function getContactsByIds(array $crmIds, array $fields): array\n {\n return $this->batchReadObjects('contacts', $crmIds, $fields);\n }\n\n /**\n * @throws CompanyApiException\n * @throws CrmException\n */\n public function getAccountById(string $crmId, array $fields): array\n {\n try {\n $company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(\n $crmId,\n implode(',', $fields),\n );\n } catch (CompanyApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch account', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $company instanceof CompaniesWithAssociations) {\n throw new CrmException('Account not found');\n }\n\n return [\n 'id' => $company->getId(),\n 'properties' => $company->getProperties(),\n ];\n }\n\n /**\n * @throws ContactApiException\n * @throws CrmException\n */\n public function getContactById(string $crmId, array $fields): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $crmId,\n implode(',', $fields)\n );\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'crm_id' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n\n if (! $contact instanceof ContactsWithAssociations) {\n throw new CrmException('Contact not found');\n }\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n }\n\n /**\n * This is email search request that Hubspot offers as GET (more generous quota)\n */\n public function getContactByEmail(string $email, array $fields = []): array\n {\n try {\n $contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(\n $email,\n implode(',', $fields),\n null,\n false,\n 'email'\n );\n\n return [\n 'id' => $contact->getId(),\n 'properties' => $contact->getProperties(),\n ];\n } catch (ContactApiException $e) {\n $this->log->info('[Hubspot] Failed to fetch contact', [\n 'email' => $email,\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n }\n\n /**\n * @throws CrmException\n */\n public function fetchProperty(string $objectType, string $propertyId): Property\n {\n $result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);\n\n if (! $result instanceof Property) {\n $this->log->error('[Hubspot] Failed to fetch property', [\n 'object_type' => $objectType,\n 'property_id' => $propertyId,\n 'reason' => $result->getMessage(),\n ]);\n\n throw new CrmException('Failed to fetch property');\n }\n\n return $result;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchPropertyOptions(string $objectType, string $propertyId): array\n {\n /** @var array<CrmFieldOption> */\n return $this->fetchProperty($objectType, $propertyId)->getOptions();\n }\n\n /**\n * @return array<array{id:string, label:string, deleted:bool}>\n */\n public function fetchCallDispositions(): array\n {\n /** @var Response $response */\n $response = $this->getInstance()->engagements()->getCallDispositions();\n\n /**\n * @var array<array{\n * id:string,\n * label:string,\n * deleted: bool\n * }>\n */\n return $response->toArray();\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityPipelineStages(): array\n {\n $stages = [];\n $apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');\n\n if ($apiResponse instanceof Error) {\n $this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $apiResponse->getMessage(),\n ]);\n\n return [];\n }\n\n foreach ($apiResponse->getResults() as $pipeline) {\n $pipelineStages = array_map(\n static function (PipelineStage $stage) {\n return [\n 'id' => $stage->getId(),\n 'label' => $stage->getLabel(),\n ];\n },\n $pipeline->getStages()\n );\n\n $stages = array_merge($stages, $pipelineStages);\n }\n\n return $stages;\n }\n\n public function fetchOpportunityPipelines(): array\n {\n $pipelines = [];\n\n try {\n $apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');\n } catch (\\Exception $e) {\n $this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [\n 'reason' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n $response = $apiResponse->toArray();\n\n foreach ($response['results'] as $pipeline) {\n $pipelines[] = [\n 'id' => $pipeline['id'],\n 'label' => $pipeline['label'],\n ];\n }\n\n return $pipelines;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchMeetingOutcomeFieldOptions(Field $field): array\n {\n return $field->getCrmProviderId() === 'meetingOutcome'\n ? $this->fetchMeetingOutcomeTypes()\n : $this->fetchCallActivityTypes();\n }\n\n public function fetchMeetingOutcomeTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/meeting/hs_meeting_outcome'\n );\n }\n\n public function fetchCallActivityTypes(): array\n {\n return $this->extractMeetingTypeOptions(\n 'https://api.hubapi.com/crm/v3/properties/call/hs_activity_type'\n );\n }\n\n private function extractMeetingTypeOptions(string $endpoint): array\n {\n /** @var Response $response */\n $response = $this->getInstance()\n ->getClient()\n ->request('GET', $endpoint);\n\n /**\n * @var array<array{\n * value: string,\n * label: string,\n * displayOrder: int\n * }> $optionData\n */\n $optionData = $response->toArray()['options'] ?? [];\n\n $options = [];\n foreach ($optionData as $item) {\n $options[] = [\n 'id' => $item['value'],\n 'value' => $item['value'],\n 'label' => $item['label'],\n 'display_order' => $item['displayOrder'],\n ];\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchDispositionFieldOptions(): array\n {\n $options = [];\n\n $dispositions = $this->fetchCallDispositions();\n\n foreach ($dispositions as $disposition) {\n if ($disposition['deleted'] !== false) {\n continue;\n }\n\n $option['value'] = $disposition['id'];\n $option['id'] = $disposition['id'];\n $option['label'] = $disposition['label'];\n\n $options[] = $option;\n }\n\n return $options;\n }\n\n /**\n * @return array<CrmFieldOption>\n */\n public function fetchOpportunityFieldOptions(Field $field): array\n {\n if ($field->isStageField()) {\n return $this->fetchOpportunityPipelineStages();\n }\n\n if ($field->isPipelineField()) {\n return $this->fetchOpportunityPipelines();\n }\n\n return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)\n {\n $endpoint = self::BASE_URL . $endpoint;\n\n if ($method === 'GET') {\n $response = $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n $response = $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\n\n $max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // \"110\"\n $remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // \"109\"\n $interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // \"10000\"\n $body = json_decode((string) $response->getBody(), true);\n\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));\n\n return $response;\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function createMeeting(array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings';\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n /**\n * @throws BadRequest\n * @throws HubspotException\n */\n public function updateMeeting(string $meetingId, array $payload): Response\n {\n $endpoint = '/crm/v3/objects/meetings/' . $meetingId;\n\n return $this->makeRequest($endpoint, 'PATCH', $payload);\n }\n\n /**\n * @throws \\Exception\n */\n public function createNote(\n string $body,\n string $ownerId,\n int $timestamp,\n string $objectId,\n NoteObject $noteObject\n ): ?string {\n try {\n $noteInput = new SimplePublicObjectInput([\n 'properties' => [\n 'hs_note_body' => $body,\n 'hubspot_owner_id' => $ownerId,\n 'hs_timestamp' => $timestamp,\n ],\n ]);\n\n // Create note\n $note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);\n\n $this->getNewInstance()->crm()->objects()->associationsApi()->create(\n 'note',\n $note->getId(),\n $this->getNoteObject($noteObject),\n $objectId,\n $this->getNoteAssociationType($noteObject),\n );\n\n return $note->getId();\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to create note', [\n 'objectId' => $objectId,\n 'noteObject' => $noteObject->getObjectType(),\n 'reason' => $e->getMessage(),\n ]);\n\n \\Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function updateEngagement(string $objectId, array $engagement, array $metadata): void\n {\n $this->getInstance()->engagements()->update($objectId, $engagement, $metadata);\n }\n\n public function getEngagementData(string $engagementId): array\n {\n $engagement = $this->getInstance()->engagements()->get($engagementId);\n\n return $engagement->toArray();\n }\n\n public function createEngagement(array $engagement, array $associations, array $metadata): Response\n {\n return $this->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n }\n\n public function isUnauthorizedException(\\Exception $e): bool\n {\n // Check for specific HubSpot API exception types first\n if ($e instanceof BadRequest) {\n // BadRequest can contain 401 status codes\n return $e->getCode() === 401;\n }\n\n // Check for HTTP client exceptions with status codes\n if ($e instanceof \\GuzzleHttp\\Exception\\RequestException && $e->hasResponse()) {\n $response = $e->getResponse();\n if ($response !== null) {\n return $response->getStatusCode() === 401;\n }\n }\n\n // Check for Guzzle HTTP exceptions\n if ($e instanceof \\GuzzleHttp\\Exception\\ClientException) {\n return $e->getCode() === 401;\n }\n\n // Fallback to string matching as last resort, but be more specific\n $message = strtolower($e->getMessage());\n\n return str_contains($message, '401 unauthorized') ||\n str_contains($message, 'http 401') ||\n str_contains($message, 'status code 401') ||\n (preg_match('/\\b401\\b/', $message) && str_contains($message, 'unauthorized'));\n }\n\n /**\n * Validates and refreshes the access token if needed before API requests.\n * This ensures long-running processes don't fail due to token expiration.\n *\n * @throws SocialAccountTokenInvalidException\n */\n public function ensureValidToken(): void\n {\n if ($this->oauthAccount === null) {\n return;\n }\n\n $newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);\n if ($newToken !== null) {\n $this->accessToken = $newToken;\n }\n }\n\n public function getConfig()\n {\n return $this->config;\n }\n\n // returns only active (archived=false)\n public function getOwners(): array\n {\n return $this->getNewInstance()->crm()->owners()->getAll();\n }\n\n /**\n * @param bool $archived\n *\n * @return array<Owner>|[]\n */\n public function getOwnersArchived(bool $archived = true): array\n {\n $endpoint = '/crm/v3/owners';\n $queryParams = [\n 'archived' => $archived ? 'true' : 'false',\n ];\n $queryString = http_build_query($queryParams);\n\n $owners = [];\n\n try {\n $response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);\n $responseData = $response?->toArray();\n\n foreach ($responseData['results'] as $result) {\n try {\n $owners[] = Owner::create($result);\n } catch (Throwable $e) {\n $this->log->error('[HubSpot] Failed to process owner data', [\n 'result' => $result,\n 'error' => $e->getMessage(),\n ]);\n\n continue;\n }\n }\n } catch (Throwable $e) {\n $this->log->error('HubSpot] Failed to fetch owners', [\n 'archived' => $archived,\n 'error' => $e->getMessage(),\n ]);\n\n return [];\n }\n\n return $owners;\n }\n\n public function getMeeting(string $engagementId): ObjectWithAssociations\n {\n return $this->getNewInstance()->crm()->objects()->basicApi()\n ->getById('meeting', $engagementId, null, 'contact,company,deal');\n }\n\n public function deleteEngagement(string $engagementId): void\n {\n $this->getInstance()->engagements()->delete((int) $engagementId);\n }\n\n public function getAssociationsData(array $ids, string $fromObject, string $toObject): array\n {\n $associationData = [];\n $idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);\n\n foreach ($idChunks as $idChunk) {\n try {\n $batchInput = new \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchInputPublicObjectId();\n $batchInput->setInputs(array_map(function ($id) {\n $publicObjectId = new \\HubSpot\\Client\\Crm\\Associations\\Model\\PublicObjectId();\n $publicObjectId->setId($id);\n\n return $publicObjectId;\n }, $idChunk));\n\n $associatedObjectsData = $this\n ->getNewInstance()\n ->crm()\n ->associations()\n ->batchApi()\n ->read($fromObject, $toObject, $batchInput);\n\n if ($associatedObjectsData instanceof \\HubSpot\\Client\\Crm\\Associations\\Model\\BatchResponsePublicAssociationMulti) {\n foreach ($associatedObjectsData->getResults() as $association) {\n $from = $association->getFrom()->getId();\n $toAssociations = $association->getTo();\n\n if (! empty($toAssociations)) {\n $associationData[$from] = array_map(function ($item) {\n return $item->getId();\n }, $toAssociations);\n }\n }\n }\n } catch (\\Exception $e) {\n $this->log->error('[Hubspot] Failed to fetch associations', [\n 'from_object' => $fromObject,\n 'to_object' => $toObject,\n 'reason' => $e->getMessage(),\n ]);\n }\n }\n\n return $associationData;\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteAssociationType(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'note_to_deal',\n NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it\n NoteObject::Account => 'note_to_company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n /**\n * @throws \\Exception\n */\n private function getNoteObject(NoteObject $noteObject): string\n {\n return match($noteObject) {\n NoteObject::Opportunity => 'deal',\n NoteObject::Lead, NoteObject::Contact => 'contact',\n NoteObject::Account => 'company',\n NoteObject::Call, NoteObject::Event => throw new \\Exception('Not supported'),\n };\n }\n\n public function addAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/create\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n\n public function removeAssociations(string $objectType, string $associationType, array $payload): Response\n {\n $endpoint = \"/crm/v4/associations/$objectType/$associationType/batch/archive\";\n\n return $this->makeRequest($endpoint, 'POST', $payload);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-188086479456983350
|
6378757184196315236
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
Editor for custom.log
Sync Changes
Hide This Notification
Code changed:
Hide
2
60
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot;
use HubSpot\Client\Crm\Deals\ApiException as DealApiException;
use HubSpot\Client\Crm\Contacts\ApiException as ContactApiException;
use HubSpot\Client\Crm\Companies\ApiException as CompanyApiException;
use HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectWithAssociations as ContactsWithAssociations;
use HubSpot\Client\Crm\Companies\Model\SimplePublicObjectWithAssociations as CompaniesWithAssociations;
use HubSpot\Client\Crm\Deals\Model\SimplePublicObjectWithAssociations as DealWithAssociations;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectInput;
use HubSpot\Client\Crm\Objects\Model\SimplePublicObjectWithAssociations as ObjectWithAssociations;
use HubSpot\Client\Crm\Pipelines\Model\Error;
use HubSpot\Client\Crm\Pipelines\Model\PipelineStage;
use HubSpot\Client\Crm\Properties\Model\Property;
use HubSpot\Discovery\Discovery;
use Jiminny\Component\Utility\Service\ProviderRateLimiter;
use Jiminny\Exceptions\CrmException;
use Jiminny\Exceptions\RateLimitException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Jobs\Crm\NoteObject;
use Jiminny\Models\Crm\Field;
use Jiminny\Services\Crm\BaseClient;
use Jiminny\Services\Crm\Hubspot\DTO\Response\Owner;
use Jiminny\Services\SocialAccountService;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use SevenShores\Hubspot\Factory;
use SevenShores\Hubspot\Http\Response;
use Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService;
use Throwable;
/**
* @phpstan-type CrmFieldOption array{id:string, label:string, value?:string}
*/
class Client extends BaseClient implements HubspotClientInterface
{
public const string MIN_API_VERSION = '2';
public const string BASE_URL = '[URL_WITH_CREDENTIALS] T
* @param callable(): T $apiCall
* @return T
*
* @throws RateLimitException
*/
private function executeRequest(callable $apiCall)
{
if (! $this->rateLimiter->canMakeRequest($this->config)) {
$retryAfter = $this->rateLimiter->requestAvailableIn($this->config);
$this->log->warning('[Hubspot] Rate limit exceeded, deferring request', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
]);
throw new RateLimitException(
'Hubspot rate limit reached for configuration ' . $this->config->getId(),
$retryAfter,
);
}
$this->rateLimiter->incrementRequestCount($this->config);
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function isHubspotRateLimit(Throwable $e): bool
{
return method_exists($e, 'getCode') && (int) $e->getCode() === 429;
}
private function parseRetryAfter(Throwable $e): int
{
if (method_exists($e, 'getResponseHeaders')) {
$headers = $e->getResponseHeaders() ?: [];
$value = $headers['Retry-After'] ?? $headers['retry-after'] ?? null;
if (is_array($value)) {
$value = $value[0] ?? null;
}
if (is_numeric($value)) {
return (int) $value;
}
}
return 10;
}
public function getMinimumApiVersion(): string
{
return self::MIN_API_VERSION;
}
public function getInstance(): Factory
{
return new Factory([
'key' => $this->accessToken,
'oauth2' => true,
'base_url' => $this->baseUrl,
]);
}
public function getNewInstance(): Discovery
{
return \HubSpot\Factory::createWithAccessToken($this->accessToken);
}
/**
* Secondly and daily limits for Hubspot API
*
* Product Tier: Free & Starter | Professional & Enterprise | API add-on (any tier)
* Burst: 100/10 seconds | 150/10 seconds | 200/10 seconds
* Daily: 250,000 | 500,000 | 1,000,000
*
* Official documentation states: The search endpoints are rate limited to five requests per second.
* Since with 5 RPS were still hitting secondly rate limits we lowered it to 4
*/
public function getPaginatedData(array $payload, string $type, int $offset = 0): array
{
$total = 0;
$lastId = null;
$rows = [];
foreach ($this->getPaginatedDataGenerator($payload, $type, $offset, $total, $lastId) as $row) {
$rows[] = $row;
}
return ['results' => $rows, 'total' => $total, 'last_record' => $lastId];
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
return $this->paginationService->getPaginatedDataGenerator(
$this,
$payload,
$type,
$offset,
$total,
$lastRecordId
);
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
$deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$crmId,
implode(',', $fields),
'companies,contacts'
));
} catch (DealApiException $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $deal instanceof DealWithAssociations) {
throw new CrmException('Deal not found');
}
return [
'id' => $deal->getId(),
'properties' => $deal->getProperties(),
'associations' => $deal->getAssociations(),
];
}
/**
* Generic batch read method for HubSpot objects
*
* @param string $objectType The object type ('deals', 'companies', 'contacts')
* @param array<string> $crmIds Array of HubSpot object IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with object data
*/
private function batchReadObjects(string $objectType, array $crmIds, array $fields): array
{
if (empty($crmIds)) {
return [];
}
$this->validateBatchSize($objectType, $crmIds);
$this->ensureValidToken();
try {
$batchConfig = $this->createBatchConfiguration($objectType);
$batchReadRequest = $this->prepareBatchRequest($batchConfig, $crmIds, $fields);
$response = $batchConfig['api']->read($batchReadRequest);
$this->validateApiResponse($response, $objectType);
$results = $this->processApiResults($response);
$this->logBatchResults($objectType, $crmIds, $results);
return $results;
} catch (\Throwable $e) {
$this->handleBatchError($e, $objectType, $crmIds);
}
}
private function validateBatchSize(string $objectType, array $crmIds): void
{
if (count($crmIds) > 100) {
throw new \InvalidArgumentException("Batch size cannot exceed 100 {$objectType}");
}
}
private function createBatchConfiguration(string $objectType): array
{
$configurations = [
'deals' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Deals\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Deals\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->deals()->batchApi(),
],
'companies' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Companies\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Companies\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->companies()->batchApi(),
],
'contacts' => [
'batchReadRequest' => new \HubSpot\Client\Crm\Contacts\Model\BatchReadInputSimplePublicObjectId(),
'inputClass' => \HubSpot\Client\Crm\Contacts\Model\SimplePublicObjectId::class,
'api' => $this->getNewInstance()->crm()->contacts()->batchApi(),
],
];
if (! isset($configurations[$objectType])) {
throw new \InvalidArgumentException("Unsupported object type: {$objectType}");
}
return $configurations[$objectType];
}
private function prepareBatchRequest(array $batchConfig, array $crmIds, array $fields): object
{
$batchReadRequest = $batchConfig['batchReadRequest'];
$inputClass = $batchConfig['inputClass'];
$inputs = array_map(function ($crmId) use ($inputClass) {
$input = new $inputClass();
$input->setId($crmId);
return $input;
}, $crmIds);
$batchReadRequest->setInputs($inputs);
$batchReadRequest->setProperties($fields);
return $batchReadRequest;
}
private function validateApiResponse($response, string $objectType): void
{
if (! $response) {
throw new CrmException("HubSpot API returned null response for {$objectType} batch read");
}
}
private function processApiResults($response): array
{
$results = [];
$responseResults = $response->getResults();
if ($responseResults) {
foreach ($responseResults as $object) {
if ($object && $object->getId()) {
$results[$object->getId()] = [
'id' => $object->getId(),
'properties' => $object->getProperties() ?: [],
];
}
}
}
return $results;
}
private function logBatchResults(string $objectType, array $crmIds, array $results): void
{
$this->log->info("[HubSpot] Batch fetched {$objectType}", [
'requested_count' => count($crmIds),
'returned_count' => count($results),
'crm_ids' => $crmIds,
]);
}
private function handleBatchError(\Throwable $e, string $objectType, array $crmIds): void
{
$errorMessage = $e->getMessage() ?: 'Unknown error';
$errorTrace = $e->getTraceAsString() ?: 'No trace available';
$this->log->error("[HubSpot] Failed to batch fetch {$objectType}", [
'crm_ids' => $crmIds,
'error' => $errorMessage,
'trace' => $errorTrace,
]);
throw new CrmException("Failed to batch fetch {$objectType}: " . $errorMessage);
}
/**
* Batch read multiple opportunities by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot deal IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with opportunity data
*/
public function getOpportunitiesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('deals', $crmIds, $fields);
}
/**
* Batch read multiple companies by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot company IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with company data
*/
public function getCompaniesByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('companies', $crmIds, $fields);
}
/**
* Batch read multiple contacts by their CRM IDs
*
* @param array<string> $crmIds Array of HubSpot contact IDs (max 100)
* @param array<string> $fields Array of property names to fetch
*
* @return array<string, array> Array keyed by CRM ID with contact data
*/
public function getContactsByIds(array $crmIds, array $fields): array
{
return $this->batchReadObjects('contacts', $crmIds, $fields);
}
/**
* @throws CompanyApiException
* @throws CrmException
*/
public function getAccountById(string $crmId, array $fields): array
{
try {
$company = $this->getNewInstance()->crm()->companies()->basicApi()->getById(
$crmId,
implode(',', $fields),
);
} catch (CompanyApiException $e) {
$this->log->info('[Hubspot] Failed to fetch account', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $company instanceof CompaniesWithAssociations) {
throw new CrmException('Account not found');
}
return [
'id' => $company->getId(),
'properties' => $company->getProperties(),
];
}
/**
* @throws ContactApiException
* @throws CrmException
*/
public function getContactById(string $crmId, array $fields): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$crmId,
implode(',', $fields)
);
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'crm_id' => $crmId,
'reason' => $e->getMessage(),
]);
throw $e;
}
if (! $contact instanceof ContactsWithAssociations) {
throw new CrmException('Contact not found');
}
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
}
/**
* This is email search request that Hubspot offers as GET (more generous quota)
*/
public function getContactByEmail(string $email, array $fields = []): array
{
try {
$contact = $this->getNewInstance()->crm()->contacts()->basicApi()->getById(
$email,
implode(',', $fields),
null,
false,
'email'
);
return [
'id' => $contact->getId(),
'properties' => $contact->getProperties(),
];
} catch (ContactApiException $e) {
$this->log->info('[Hubspot] Failed to fetch contact', [
'email' => $email,
'reason' => $e->getMessage(),
]);
return [];
}
}
/**
* @throws CrmException
*/
public function fetchProperty(string $objectType, string $propertyId): Property
{
$result = $this->getNewInstance()->crm()->properties()->coreApi()->getByName($objectType, $propertyId);
if (! $result instanceof Property) {
$this->log->error('[Hubspot] Failed to fetch property', [
'object_type' => $objectType,
'property_id' => $propertyId,
'reason' => $result->getMessage(),
]);
throw new CrmException('Failed to fetch property');
}
return $result;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchPropertyOptions(string $objectType, string $propertyId): array
{
/** @var array<CrmFieldOption> */
return $this->fetchProperty($objectType, $propertyId)->getOptions();
}
/**
* @return array<array{id:string, label:string, deleted:bool}>
*/
public function fetchCallDispositions(): array
{
/** @var Response $response */
$response = $this->getInstance()->engagements()->getCallDispositions();
/**
* @var array<array{
* id:string,
* label:string,
* deleted: bool
* }>
*/
return $response->toArray();
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityPipelineStages(): array
{
$stages = [];
$apiResponse = $this->getNewInstance()->crm()->pipelines()->pipelinesApi()->getAll('deals');
if ($apiResponse instanceof Error) {
$this->log->error('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $apiResponse->getMessage(),
]);
return [];
}
foreach ($apiResponse->getResults() as $pipeline) {
$pipelineStages = array_map(
static function (PipelineStage $stage) {
return [
'id' => $stage->getId(),
'label' => $stage->getLabel(),
];
},
$pipeline->getStages()
);
$stages = array_merge($stages, $pipelineStages);
}
return $stages;
}
public function fetchOpportunityPipelines(): array
{
$pipelines = [];
try {
$apiResponse = $this->makeRequest('/crm/v3/pipelines/deals');
} catch (\Exception $e) {
$this->log->info('[Hubspot] Failed to fetch opportunity pipelines', [
'reason' => $e->getMessage(),
]);
return [];
}
$response = $apiResponse->toArray();
foreach ($response['results'] as $pipeline) {
$pipelines[] = [
'id' => $pipeline['id'],
'label' => $pipeline['label'],
];
}
return $pipelines;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchMeetingOutcomeFieldOptions(Field $field): array
{
return $field->getCrmProviderId() === 'meetingOutcome'
? $this->fetchMeetingOutcomeTypes()
: $this->fetchCallActivityTypes();
}
public function fetchMeetingOutcomeTypes(): array
{
return $this->extractMeetingTypeOptions(
'[URL_WITH_CREDENTIALS] Response $response */
$response = $this->getInstance()
->getClient()
->request('GET', $endpoint);
/**
* @var array<array{
* value: string,
* label: string,
* displayOrder: int
* }> $optionData
*/
$optionData = $response->toArray()['options'] ?? [];
$options = [];
foreach ($optionData as $item) {
$options[] = [
'id' => $item['value'],
'value' => $item['value'],
'label' => $item['label'],
'display_order' => $item['displayOrder'],
];
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchDispositionFieldOptions(): array
{
$options = [];
$dispositions = $this->fetchCallDispositions();
foreach ($dispositions as $disposition) {
if ($disposition['deleted'] !== false) {
continue;
}
$option['value'] = $disposition['id'];
$option['id'] = $disposition['id'];
$option['label'] = $disposition['label'];
$options[] = $option;
}
return $options;
}
/**
* @return array<CrmFieldOption>
*/
public function fetchOpportunityFieldOptions(Field $field): array
{
if ($field->isStageField()) {
return $this->fetchOpportunityPipelineStages();
}
if ($field->isPipelineField()) {
return $this->fetchOpportunityPipelines();
}
return $this->fetchPropertyOptions('deals', $field->getCrmProviderId());
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function makeRequest(string $endpoint, $method = 'GET', $payload = [], ?string $queryString = null)
{
$endpoint = self::BASE_URL . $endpoint;
if ($method === 'GET') {
$response = $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
$response = $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
$max = $response->getHeaderLine('X-HubSpot-RateLimit-Max'); // "110"
$remaining = $response->getHeaderLine('X-HubSpot-RateLimit-Remaining'); // "109"
$interval = $response->getHeaderLine('X-HubSpot-RateLimit-Interval-Milliseconds'); // "10000"
$body = json_decode((string) $response->getBody(), true);
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$max ' . PHP_EOL . print_r($max, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$remaining ' . PHP_EOL . print_r($remaining, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$interval ' . PHP_EOL . print_r($interval, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$body ' . PHP_EOL . print_r($body, true));
return $response;
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function createMeeting(array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings';
return $this->makeRequest($endpoint, 'POST', $payload);
}
/**
* @throws BadRequest
* @throws HubspotException
*/
public function updateMeeting(string $meetingId, array $payload): Response
{
$endpoint = '/crm/v3/objects/meetings/' . $meetingId;
return $this->makeRequest($endpoint, 'PATCH', $payload);
}
/**
* @throws \Exception
*/
public function createNote(
string $body,
string $ownerId,
int $timestamp,
string $objectId,
NoteObject $noteObject
): ?string {
try {
$noteInput = new SimplePublicObjectInput([
'properties' => [
'hs_note_body' => $body,
'hubspot_owner_id' => $ownerId,
'hs_timestamp' => $timestamp,
],
]);
// Create note
$note = $this->getNewInstance()->crm()->objects()->basicApi()->create('note', $noteInput);
$this->getNewInstance()->crm()->objects()->associationsApi()->create(
'note',
$note->getId(),
$this->getNoteObject($noteObject),
$objectId,
$this->getNoteAssociationType($noteObject),
);
return $note->getId();
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to create note', [
'objectId' => $objectId,
'noteObject' => $noteObject->getObjectType(),
'reason' => $e->getMessage(),
]);
\Sentry::captureException($e);
}
return null;
}
public function updateEngagement(string $objectId, array $engagement, array $metadata): void
{
$this->getInstance()->engagements()->update($objectId, $engagement, $metadata);
}
public function getEngagementData(string $engagementId): array
{
$engagement = $this->getInstance()->engagements()->get($engagementId);
return $engagement->toArray();
}
public function createEngagement(array $engagement, array $associations, array $metadata): Response
{
return $this->getInstance()
->engagements()
->create($engagement, $associations, $metadata);
}
public function isUnauthorizedException(\Exception $e): bool
{
// Check for specific HubSpot API exception types first
if ($e instanceof BadRequest) {
// BadRequest can contain 401 status codes
return $e->getCode() === 401;
}
// Check for HTTP client exceptions with status codes
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$response = $e->getResponse();
if ($response !== null) {
return $response->getStatusCode() === 401;
}
}
// Check for Guzzle HTTP exceptions
if ($e instanceof \GuzzleHttp\Exception\ClientException) {
return $e->getCode() === 401;
}
// Fallback to string matching as last resort, but be more specific
$message = strtolower($e->getMessage());
return str_contains($message, '401 unauthorized') ||
str_contains($message, 'http 401') ||
str_contains($message, 'status code 401') ||
(preg_match('/\b401\b/', $message) && str_contains($message, 'unauthorized'));
}
/**
* Validates and refreshes the access token if needed before API requests.
* This ensures long-running processes don't fail due to token expiration.
*
* @throws SocialAccountTokenInvalidException
*/
public function ensureValidToken(): void
{
if ($this->oauthAccount === null) {
return;
}
$newToken = $this->tokenManager->ensureValidToken($this->oauthAccount);
if ($newToken !== null) {
$this->accessToken = $newToken;
}
}
public function getConfig()
{
return $this->config;
}
// returns only active (archived=false)
public function getOwners(): array
{
return $this->getNewInstance()->crm()->owners()->getAll();
}
/**
* @param bool $archived
*
* @return array<Owner>|[]
*/
public function getOwnersArchived(bool $archived = true): array
{
$endpoint = '/crm/v3/owners';
$queryParams = [
'archived' => $archived ? 'true' : 'false',
];
$queryString = http_build_query($queryParams);
$owners = [];
try {
$response = $this->makeRequest(endpoint: $endpoint, queryString: $queryString);
$responseData = $response?->toArray();
foreach ($responseData['results'] as $result) {
try {
$owners[] = Owner::create($result);
} catch (Throwable $e) {
$this->log->error('[HubSpot] Failed to process owner data', [
'result' => $result,
'error' => $e->getMessage(),
]);
continue;
}
}
} catch (Throwable $e) {
$this->log->error('HubSpot] Failed to fetch owners', [
'archived' => $archived,
'error' => $e->getMessage(),
]);
return [];
}
return $owners;
}
public function getMeeting(string $engagementId): ObjectWithAssociations
{
return $this->getNewInstance()->crm()->objects()->basicApi()
->getById('meeting', $engagementId, null, 'contact,company,deal');
}
public function deleteEngagement(string $engagementId): void
{
$this->getInstance()->engagements()->delete((int) $engagementId);
}
public function getAssociationsData(array $ids, string $fromObject, string $toObject): array
{
$associationData = [];
$idChunks = array_chunk($ids, self::ASSOCIATIONS_BATCH_SIZE_LIMIT);
foreach ($idChunks as $idChunk) {
try {
$batchInput = new \HubSpot\Client\Crm\Associations\Model\BatchInputPublicObjectId();
$batchInput->setInputs(array_map(function ($id) {
$publicObjectId = new \HubSpot\Client\Crm\Associations\Model\PublicObjectId();
$publicObjectId->setId($id);
return $publicObjectId;
}, $idChunk));
$associatedObjectsData = $this
->getNewInstance()
->crm()
->associations()
->batchApi()
->read($fromObject, $toObject, $batchInput);
if ($associatedObjectsData instanceof \HubSpot\Client\Crm\Associations\Model\BatchResponsePublicAssociationMulti) {
foreach ($associatedObjectsData->getResults() as $association) {
$from = $association->getFrom()->getId();
$toAssociations = $association->getTo();
if (! empty($toAssociations)) {
$associationData[$from] = array_map(function ($item) {
return $item->getId();
}, $toAssociations);
}
}
}
} catch (\Exception $e) {
$this->log->error('[Hubspot] Failed to fetch associations', [
'from_object' => $fromObject,
'to_object' => $toObject,
'reason' => $e->getMessage(),
]);
}
}
return $associationData;
}
/**
* @throws \Exception
*/
private function getNoteAssociationType(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'note_to_deal',
NoteObject::Lead, NoteObject::Contact => 'note_to_contact', // or 'note_to_lead' if your portal supports it
NoteObject::Account => 'note_to_company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
/**
* @throws \Exception
*/
private function getNoteObject(NoteObject $noteObject): string
{
return match($noteObject) {
NoteObject::Opportunity => 'deal',
NoteObject::Lead, NoteObject::Contact => 'contact',
NoteObject::Account => 'company',
NoteObject::Call, NoteObject::Event => throw new \Exception('Not supported'),
};
}
public function addAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/create";
return $this->makeRequest($endpoint, 'POST', $payload);
}
public function removeAssociations(string $objectType, string $associationType, array $payload): Response
{
$endpoint = "/crm/v4/associations/$objectType/$associationType/batch/archive";
return $this->makeRequest($endpoint, 'POST', $payload);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
2848
|
NULL
|
NULL
|
NULL
|
|
2848
|
114
|
39
|
2026-05-07T11:43:19.497998+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778154199497_m2.jpg...
|
PhpStorm
|
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
make
Inherited members (⌘R)
Anonymous Classes (⌘I) make
Inherited members (⌘R)
Anonymous Classes (⌘I)
Lambdas (⌘L)
Client, class
makeRequest(endpoint: string, [method: string = 'GET'], [payload: array = [...]], [queryString: null|string = null]): null|ResponseInterface|Response, public method
makeRequest(endpoint: string, [method: string = 'GET'], [payload: array = [...]], [queryString: null|string = null]): null|ResponseInterface|Response, public method
Client.php...
|
[{"role":"AXTextField","text [{"role":"AXTextField","text":"make","depth":1,"bounds":{"left":0.5212766,"top":0.31444532,"width":0.030917553,"height":0.022346368},"on_screen":true,"value":"make","role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Inherited members (⌘R)","depth":1,"bounds":{"left":0.5242686,"top":0.33998403,"width":0.052526597,"height":0.022346368},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Anonymous Classes (⌘I)","depth":1,"bounds":{"left":0.58011967,"top":0.33998403,"width":0.052526597,"height":0.022346368},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Lambdas (⌘L)","depth":1,"bounds":{"left":0.6359708,"top":0.33998403,"width":0.052526597,"height":0.022346368},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Client, class","depth":4,"bounds":{"left":0.5299202,"top":0.36472467,"width":0.019946808,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"makeRequest(endpoint: string, [method: string = 'GET'], [payload: array = [...]], [queryString: null|string = null]): null|ResponseInterface|Response, public method","depth":5,"bounds":{"left":0.5362367,"top":0.38228253,"width":0.30917552,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"makeRequest(endpoint: string, [method: string = 'GET'], [payload: array = [...]], [queryString: null|string = null]): null|ResponseInterface|Response, public method","depth":4,"bounds":{"left":0.5362367,"top":0.38228253,"width":0.30917552,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"Client.php","depth":1,"bounds":{"left":0.5242686,"top":0.31364724,"width":0.19980054,"height":0.026336791},"on_screen":true,"role_description":"text"}]...
|
-8185782019858167655
|
-4592230567921954007
|
visual_change
|
accessibility
|
NULL
|
make
Inherited members (⌘R)
Anonymous Classes (⌘I) make
Inherited members (⌘R)
Anonymous Classes (⌘I)
Lambdas (⌘L)
Client, class
makeRequest(endpoint: string, [method: string = 'GET'], [payload: array = [...]], [queryString: null|string = null]): null|ResponseInterface|Response, public method
makeRequest(endpoint: string, [method: string = 'GET'], [payload: array = [...]], [queryString: null|string = null]): null|ResponseInterface|Response, public method
Client.php...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
2847
|
114
|
38
|
2026-05-07T11:43:10.360371+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778154190360_m2.jpg...
|
PhpStorm
|
faVsco.js – Hubspot/Client.php
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
8043719072324535154
|
-8628527368849355612
|
visual_change
|
hybrid
|
NULL
|
Project: faVsco.js, menu
PhostormINavigarecodeLara Project: faVsco.js, menu
PhostormINavigarecodeLaravelS0 lho# Support Daily - in 17 m100% CThu 7 May 14:43:10FV faVsco.jsAskJiminnyReportActivityServiceTest vProiectRematchActivityOnCrmObjectDetach.phpT DeleteCrmEntityTrait.php= custom.log X = laravel.logA SF [jiminny@localhost]HS_local (jiminny@localhost]# console [PKol)A console (eu)« console [STAGING]v D ServiceTraitsT OpportunitySync1© RateLimitException.phpAddkateLimitcommand.pnp© Hubspot/Client.php X | © SyncOpportunity.php© WebhookSyncBatchProcessor.phpu syncermentitesT SyncFieldsTrait.plT WriteCrmTrait.phHip/RateLimited.php© BaseRateLimiter.phpC) service.phg0 SyncCrmEntitiesTrait.php© OpportunitySyncTest.php© SyncTeamMetadata.phpuRateLimitintenace.oho© RateLimiterInstance.php•DUuis> D Webhookclass cuzent extends sasectzent ampLements hubspete entintertace таст оатаc) Batchsynccollector.f© BatchSvncRedisServc) Client.php© ClosedDealStagesSeDealFieldsService.phC)DecorateActiviv.onC) FieldivoeConverter.t1) HubsootClientinterfa(C) HubsootTokenMana‹© PayloadBuilder.php© RemoteCrmObjectM:YC) RocnonseNormalize 1© Service.php© SyncFieldAction.php© SyncRelatedActivityN© WebhookSyncBatchf• D IntegrationApp› D Accessors444447> D Confia> DDTO• W Filters> D Jobs› _ Prospectsearchstrat›D ServiceTraitsc) Dataclient.phpc)DecorateActivtv.oneC)Loca|Search.ohv1) LocalSearchinterface458(C) RemoteSearch.ono(C) Service,ohov m ListenersC) ConvertleadActivitie462C) Purael.ookuncache.r> M MetadataTM Miarationl469>D Pipedrive• M Salesforce> M Siolde>@ OpportunityMatcher› D OpportunitySyncStra) M DracnontCoarchStrat• M CorvicoTraitepublic function getContactsByIds(array ScrmIds, array $fields): array{...}* actows compangapiexcepczon* Athrows CrmSycentionpublic function getAccountById(string $crmId, array $fields): array{...}* dchrows conzactApltxceptionpubLic tunction getlontactbyld string scrmla, array stlelas: arrayScontact = Sthis->qetNewInstance@->crm@->contacts@->basicApi@->qetBvTd(imolode separato,''. Sfields)catch ContactAoiExceotion Se) <$this->log->info('[Hubspot] Failed to fetch contact', [reason' => Se->aetMessageOi.throw Se:if(l Scontact inctanconf ContactcWithAccociationc){thoow new CrmFycention( messaae,'Contact not found'):return('id' => $contact->getIdo.'properties' => Scontact->aetPronentjoc()l* This is email search request that Hubspot &efers asAccept File &-rQY Reiuffile onenar@ube for IDE suanestions. Detect.more securityissuecin.vour.DLD.files//Tin/Sonar@ube Cloud for free//Download.Sonar@ube.Senver_/llLeam.mn't ack adain (todav 10-25)447:33 UTF-8 # 4 spaces...
|
2846
|
NULL
|
NULL
|
NULL
|
|
2846
|
114
|
37
|
2026-05-07T11:43:07.424091+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778154187424_m2.jpg...
|
PhpStorm
|
faVsco.js – SyncCrmEntitiesTrait.php
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
Editor for custom.log...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.034242023,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: master","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"Editor for custom.log","depth":4,"bounds":{"left":0.5475399,"top":0.0726257,"width":0.44082448,"height":0.9066241},"on_screen":true,"role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
61097548596911936
|
-8132287983061464126
|
click
|
hybrid
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
Editor for custom.log
PhostormINavigarecodeLaravelKeractorFV faVsco.jsProiect vRematchActivityOnCrmObjectDetach.phpT DeleteCrmEntityTrait.phpv D ServiceTraitsT OpportunitySync1© RateLimitException.phpAddkateLimitcommand.php© Hubspot/Client.php© SyncOpportunity.php© WebhookSyncBatchProcessor.php© ImportOpportunityBatch.phpu syncermentites iu syncrielasiralt.olHip/RateLimited.php© BaseRateLimiter.php© Service.phpT SyncCrmEntitiesTrait.php XT WriteCrmTrait.ph•DUuis> D Webhook© OpportunitySyncTest.php© RateLimiterInstance.php© ProviderRateLimiter.phptrait synccrmentitzestraitM| A62 X32 A Vc) Batchsynccollector.© BatchSvncRedisServC) Client.php(c) ClosedDealStagesSeDealFieldsService.ph@ DecorateActivitv.ohrcFieldDerinitions.onvC) FieldivoeConverter.n1) HubsootClientinterfa(C) HubsootTokenMana‹71 6f© PayloadBuilder.php@ PemoteCrmObiectM:YC) RocnonseNormalize 1© Service.php© SyncFieldAction.php 100 ai ©© SyncRelatedActivity^ 101© WebhookSyncBatchF 102• D IntegrationApp› D Accessorscontio> DDTO• W Filters> D Jobs• C ProspectSearchStrat 110)›D ServiceTraitsc) Dataclient.phpc) DecorateActivtv.ono 115C)LocalSearch.ohv1) LocalSearchinterface 115)(C) RemoteSearch.ono(C) Service,ohov m ListenersC) Convertl eadActivitie 119C) Purael.ookuncache.r 120> M Metadata• M Miaration>D Pipedrive• M Salesforce> M Siolde>@ OpportunityMatcher› D OpportunitySyncStra 127M DrocnontCaarchCtrat 129• M CorvicoTraite* - Backfill operations* ror regular sync webnook bacchsyncconcacts is used.* doaram carbon sSince reuch concacus moulried arcer chis date: Qparam CarbonInulz Sto Optional end date for modification rangepublic function svncContacts(Carbon Ssince. ?Carbon Sto = null): int{...}* Ginheritdodnublic function svncContact(strina Scrmid: ?Contacttryif (! Sthis->client instanceof HubspotClientInterface) i+hnow new TnvalidAraumen+Sycentiond medage: 'Client must implement HubspotClientInterface');$fields = $this->getContactFields:$hsContact = Sthis->client->getContactById($crmId, $fields):} catch (\HubSpot\Client\Crm\Contacts'Chanco NodlaratinnSthis->logger->info('[' . $this->gm d getContactByld (HubspotClientinterface .../app/Services/Crm/Hubspo"ceamld" = schls->rean->geruu'crmId' => $crmId'reason' => $e->qetMessageOO e getcontactiyld (cLient .../app/Servzces/crm/Hubspot)1);} catch (CrmException Se) {Sthis->loqger->1nfo('|'• Sthis->qetd1solavNameo.'] Contact not found', ["teamid => sthus->team->cetuuido'reason' => Se->aetMessageOl14iemntv(ChstontactiinronentiectilemntvShctontactttidr1nncSthis-sloagen-swanning(trt Sthis->aethisnlavNameo."Contact data inconnletel."teamtd' => Sthis->team->aetlluidoarAube for IDE suanestionsa Detect more seawritvlssues lin vour D.Dffiles lltn SonarAube Claud for free //lDownload SonarOmbe Server /llllear more /llDonit ask adain /itodav 10525111 1111= custom.log X = laravel.log4 SF jjiminny@localhost]HS_local (jiminny@localhost]# console [PKol)A console (eu)S0 lho# Support Daily - in 17 mU AskJiminnyReportActivityServiceTest v« console [STAGING]100% CThu 7 May 14:43:07WN Windsurf Teamc108:47 UTF-8 # 4 spaces...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
2845
|
113
|
40
|
2026-05-07T11:43:07.540128+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778154187540_m1.jpg...
|
PhpStorm
|
faVsco.js – Hubspot/Client.php
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"on_screen":true,"help_text":"Git Branch: master","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-4235983745889776938
|
-8204421443435123770
|
click
|
hybrid
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
iTerm2ShellEditViewSessionScriptsProfilesWindowHelp$0Support Daily - in 17 m100% <478DEV (docker)DOCKERDEV (docker)882APP (-zsh)-zshjiminny-worker-processing-4:j1minny-worker-processing-4_00:jiminny-worker-processing-5:jiminny-worker-processing-5_00:stoppedstoppedworker-analytics:worker-analytics_00: stoppedworker-crm-update:worker-crm-update_00: stoppedartisan-schedule:artisan-schedule_00:stoppedworker-nudges:worker-nudges_00: stoppedworker:worker_00: stoppedjiminny-worker-processing-1:jiminny-worker-processing-1_00: stoppedworker-audio:worker-audio_00: stoppedworker-calendar:worker-calendar_00:stoppedworker-conferences:worker-conferences_00:stoppedworker-crm-sync:worker-crm-sync_00:stoppedworker-emails:worker-emails_00:stoppedworker-es-update:worker-es-update_00: stoppedartisan-schedule:artisan-schedule_00:startedjiminny-worker-processing-1:jiminny-worker-processing-1_00: startedjiminny-worker-processing-2:jiminny-worker-processing-2_00: startedjiminny-worker-processing-3:jiminny-worker-processing-3_00: startedjiminny-worker-processing-4:jiminny-worker-processing-4_00: startedjiminny-worker-processing-5:jiminny-worker-processing-5_00: startedjiminny-worker-processing-delayed: jiminny-worker-processing-delayed_00: startedworker:worker_00: startedworker-analytics:worker-analytics_00: startedworker-audio:worker-audio_00: startedworker-calendar:worker-calendar_00: startedworker-conferences:worker-conferences_00: startedworker-crm-sync:worker-crm-sync_00: startedworker-crm-update:worker-crm-update_00: startedworker-download:worker-download_00:startedworker-emails:worker-emails_00: startedworker-es-update:worker-es-update_00: startedworker-nudges:worker-nudges_00: startedroot@docker_lamp_1:/home/jiminny# php artisan crm:sync-opportunity --teamId=2 --from='2026-05-01 00:00:00'Syncing opportunity for HubspotSyncing opportunities modified since 2026-05-01 00:00:00...Synced 6 opportunities.root@docker_lamp_1:/home/jiminny# php artisan crm:sync-opportunity --teamId=2 --opportunityId 374720564Syncing opportunity for HubspotSyncing opportunity 374720564...Synced AmirHSOpp to 5066root@docker_lamp_1:/home/jiminny# php artisan crm: sync-contact --teamId=2 --contactId 21351Syncing contact(s) for HubspotSyncing contact 21351...Synced Lissy Newlandto 464root@docker_lamp_1:/home/jiminny# ]• $4screenpipe*•$5-zshThu 7 May 14:43:07T81₴6DEV...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
2844
|
113
|
39
|
2026-05-07T11:43:04.828615+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778154184828_m1.jpg...
|
PhpStorm
|
faVsco.js – SyncCrmEntitiesTrait.php
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
Editor for custom.log
Sync Changes
Hide This Notification
Code changed:
Hide
Analyzing…
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\ServiceTraits;
use Carbon\Carbon;
use Exception;
use Illuminate\Support\Str;
use Jiminny\Exceptions\CrmException;
use Jiminny\Jobs\Crm\Delete\DeleteAccountJob;
use Jiminny\Jobs\Crm\Delete\DeleteContactJob;
use Jiminny\Jobs\Crm\Delete\DeleteOpportunityJob;
use Jiminny\Models\Account;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Repositories\Crm\CrmEntityRepository;
use Jiminny\Services\Avatar\ProspectPhotoPathService;
use Jiminny\Services\Crm\Hubspot\HubspotClientInterface;
use Jiminny\Services\Crm\Hubspot\WebhookSyncBatchProcessor;
use Jiminny\Utils\StringUtil;
trait SyncCrmEntitiesTrait
{
use OpportunitySyncTrait;
private const string CDN_URL = '[URL_WITH_CREDENTIALS] Carbon $since Fetch contacts modified after this date
* @param Carbon|null $to Optional end date for modification range
*
* @return int Number of contacts successfully synced
*/
public function syncContacts(Carbon $since, ?Carbon $to = null): int
{
$syncCount = 0;
try {
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$fields = $this->getContactFields();
$payload = $this->payloadBuilder->getRecentlyUpdatedSearchPayload($since, $to, $fields);
foreach ($this->client->getPaginatedDataGenerator($payload, 'contacts') as $hsContact) {
$this->importContact($hsContact);
$syncCount++;
}
} catch (Exception $exception) {
$this->logger->error('[' . $this->getDisplayName() . '] Sync contacts failed', [
'teamId' => $this->team->getUuid(),
'reason' => $exception->getMessage(),
]);
}
return $syncCount;
}
/**
* @inheritdoc
*/
public function syncContact(string $crmId): ?Contact
{
try {
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$fields = $this->getContactFields();
$hsContact = $this->client->getContactById($crmId, $fields);
} catch (\HubSpot\Client\Crm\Contacts\ApiException $e) {
$this->logger->info('[' . $this->getDisplayName() . '] Contact fetch failed', [
'teamId' => $this->team->getUuid(),
'crmId' => $crmId,
'reason' => $e->getMessage(),
]);
return null;
} catch (CrmException $e) {
$this->logger->info('[' . $this->getDisplayName() . '] Contact not found', [
'teamId' => $this->team->getUuid(),
'crmId' => $crmId,
'reason' => $e->getMessage(),
]);
return null;
}
if (empty($hsContact['properties']) || empty($hsContact['id'])) {
$this->logger->warning('[' . $this->getDisplayName() . '] Contact data incomplete', [
'teamId' => $this->team->getUuid(),
'crmId' => $crmId,
'has_properties' => ! empty($hsContact['properties']),
'has_id' => ! empty($hsContact['id']),
]);
return null;
}
return $this->importContact($hsContact);
}
private function getContactFields(): array
{
return [
'associatedcompanyid',
'country',
'firstname',
'lastname',
'phone',
'mobilephone',
'email',
'photo',
'hs_avatar_filemanager_key',
'jobtitle',
'hubspot_owner_id',
'hs_object_id',
'createdate',
'hs_lastmodifieddate',
];
}
/**
* @inheritdoc
*/
private function importContact($crmData, array $accountMappings = []): ?Contact
{
$crmProviderId = $crmData['id'] ?? null;
$this->logger->info('[HubSpot] importContact', [
'crm_provider_id' => $crmProviderId,
'config_id' => $this->config->getId(),
]);
if (empty($crmData['properties'])) {
$this->logger->info('[HubSpot] importContact failed: empty properties', [
'crm_provider_id' => $crmProviderId,
'config_id' => $this->config->getId(),
]);
return null;
}
$properties = $crmData['properties'];
$crmId = (string) $crmData['id'];
$accountId = $this->resolveContactAccount($properties, $accountMappings);
$data = $this->buildContactData($crmId, $properties, $accountId);
return $this->crmEntityRepository->importContact($this->config, $data);
}
private function resolveContactAccount(array $properties, array $accountMappings): ?int
{
if (empty($properties['associatedcompanyid'])) {
return null;
}
$companyId = (string) $properties['associatedcompanyid'];
if (! empty($accountMappings)) {
return $accountMappings[$companyId] ?? null;
}
return $this->crmEntityRepository->findAccountByExternalId(
$this->team->getCrmConfiguration(),
$companyId
)?->getId() ?? $this->syncAccount($companyId)?->getId();
}
private function buildContactData(string $crmId, array $properties, ?int $accountId): array
{
$countryCode = $this->buildContactCountry($properties);
$name = $this->buildContactName($properties);
$photoPath = $this->teamService->generateAvatar(
$crmId,
empty($name) ? ($properties['email'] ?? 'N/A') : $name,
);
$parsedNumber = $this->buildContactPhone($countryCode, $properties);
$mobileNumber = $this->buildContactMobilePhone($countryCode, $properties);
$ownerId = $properties['hubspot_owner_id'] ?? null;
$profile = $ownerId !== null
? $this->crmEntityRepository->findProfileByExternalId($this->config, (string) $ownerId)
: null;
$ext = (isset($parsedNumber['ext']) && is_string($parsedNumber['ext']) && strlen($parsedNumber['ext']) <= 10)
? $parsedNumber['ext']
: null;
$title = isset($properties['jobtitle']) ? mb_strimwidth($properties['jobtitle'], 0, 128) : null;
$email = isset($properties['email']) ? mb_strimwidth($properties['email'], 0, 191) : null;
$remotelyCreatedAt = ! empty($properties['createdate']) ? Carbon::parse($properties['createdate']) : null;
return [
'crm_provider_id' => $crmId,
'team_id' => $this->team->getId(),
'account_id' => $accountId,
'user_id' => $profile?->getUserId(),
'owner_id' => $ownerId,
'name' => $name,
'title' => $title,
'email' => $email,
'country_code' => $countryCode,
'phone' => $parsedNumber['phone'] ?? null,
'mobile_phone' => $mobileNumber ?? null,
'ext' => $ext,
'photo_path' => $photoPath,
'remotely_created_at' => $remotelyCreatedAt,
];
}
/**
* @param $properties
*/
private function buildContactName($properties): string
{
if (is_array($properties)) {
return $this->buildContactNameFromArray($properties);
}
return $this->buildContactNameFromObject($properties);
}
private function buildContactNameFromArray(array $properties): string
{
if (! empty($properties['name'])) {
return mb_strimwidth($properties['name'], 0, 100);
}
$name = '';
if (! empty($properties['firstname'])) {
$name = $properties['firstname'] . ' ';
}
if (! empty($properties['lastname'])) {
$name .= $properties['lastname'];
}
if ($name === '' && ! empty($properties['email'])) {
$name = $properties['email'];
}
return mb_strimwidth($name, 0, 100);
}
private function buildContactNameFromObject($properties): string
{
$name = '';
if (isset($properties->firstname)) {
$name = $properties->firstname->value . ' ';
}
if (isset($properties->lastname)) {
$name .= $properties->lastname->value;
}
if ($name === '' && isset($properties->email)) {
$name = $properties->email->value;
}
return mb_strimwidth($name, 0, 100);
}
/**
* @param $properties
*/
private function buildContactPhone(?string $countryCode, $properties): ?array
{
if (is_array($properties) && empty($properties['phone']) === false) {
$number = mb_strimwidth($properties['phone'], 0, 25);
return parsePhoneNumber($countryCode, $number);
} elseif (isset($properties->phone)) {
$number = mb_strimwidth($properties->phone->value, 0, 25);
return parsePhoneNumber($countryCode, $number);
}
return [];
}
/**
* @param $properties
*/
private function buildContactMobilePhone(?string $countryCode, $properties): ?string
{
return isset($properties['mobilephone'])
? Str::limit(phone_e164($countryCode, $properties['mobilephone']), 25, '')
: null;
}
/**
* @param $properties
* @param $account
*/
private function buildContactCountry($properties): ?string
{
if (is_array($properties) && empty($properties['country']) === false) {
return $this->convertCountryNameToCode($properties['country']);
}
if (isset($properties->country)) {
return $this->convertCountryNameToCode($properties->country->value);
}
return null;
}
/**
* HubSpot doesn't have leads, so this method does nothing.
*
* @param Carbon $since
* @param Carbon|null $to
* @param string|null $crmProfileId
*
* @return int
*/
public function syncLeads(Carbon $since, ?Carbon $to = null, ?string $crmProfileId = null): int
{
// Mark unused parameters to avoid code smell warnings
unset($since, $to, $crmProfileId);
return 0;
}
/**
* HubSpot doesn't have leads.
*
* @param string $crmId
*
* @inheritdoc
*/
public function syncLead(string $crmId): ?Lead
{
// Mark unused parameter to avoid code smell warnings
unset($crmId);
return null;
}
/**
* Sync accounts (companies) modified since a given date (manual sync mode).
*
* This method fetches companies from HubSpot API based on modification date and
* imports them one by one. It is used for:
* - Manual sync commands (e.g., crm:sync-account with --from parameter)
* - Initial sync for new teams
* - Backfill operations
*
* For regular sync webhook batchSyncCompanies is used:
*
* @param Carbon $since Fetch companies modified after this date
* @param Carbon|null $to Optional end date for modification range
*
* @return int Number of companies successfully synced
*/
public function syncAccounts(Carbon $since, ?Carbon $to = null): int
{
$syncCount = 0;
try {
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$fields = $this->getCompanyFields();
$payload = $this->payloadBuilder->getRecentlyUpdatedSearchPayload($since, $to, $fields);
foreach ($this->client->getPaginatedDataGenerator($payload, 'companies') as $hsAccount) {
$this->importAccount($hsAccount);
$syncCount++;
}
} catch (Exception $exception) {
$this->logger->error('[' . $this->getDisplayName() . '] Sync accounts failed', [
'teamId' => $this->team->getUuid(),
'reason' => $exception->getMessage(),
]);
}
return $syncCount;
}
/**
* @inheritdoc
*/
public function syncAccount(string $crmId): ?Account
{
try {
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$fields = $this->getCompanyFields();
$hsAccount = $this->client->getAccountById($crmId, $fields);
} catch (\HubSpot\Client\Crm\Companies\ApiException $e) {
$this->logger->info('[' . $this->getDisplayName() . '] Account fetch failed', [
'teamId' => $this->team->getUuid(),
'crmId' => $crmId,
'reason' => $e->getMessage(),
]);
return null;
} catch (CrmException $e) {
$this->logger->info('[' . $this->getDisplayName() . '] Account not found', [
'teamId' => $this->team->getUuid(),
'crmId' => $crmId,
'reason' => $e->getMessage(),
]);
return null;
}
return $this->importAccount($hsAccount);
}
/**
* Process webhook-collected contact batches.
*
* Drains Redis sets containing contact CRM IDs collected from webhook events
* and dispatches ImportContactBatch jobs for batch processing.
*
* @return int Number of contact IDs dispatched to jobs
*/
public function batchSyncContacts(): int
{
$configId = $this->team->getCrmConfiguration()->getId();
return $this->batchProcessor->processBatchesForObjectType(
WebhookSyncBatchProcessor::OBJECT_TYPE_CONTACT,
$configId
);
}
public function importContactBatchByIds(array $crmIds): array
{
$result = [
'success_count' => 0,
'failed_ids' => [],
'errors' => [],
];
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$batchStart = microtime(true);
$slowContacts = [];
$fetchStart = microtime(true);
$allContacts = $this->fetchContactsByIdsInChunks($crmIds);
$fetchMs = (int) round((microtime(true) - $fetchStart) * 1000);
$fetchedIds = array_map('strval', array_column($allContacts, 'id'));
$notFoundIds = array_values(array_diff(array_map('strval', $crmIds), $fetchedIds));
if (! empty($notFoundIds)) {
$this->logger->info('[' . $this->getDisplayName() . '] Contact CRM IDs not found in HubSpot', [
'teamId' => $this->team->getId(),
'notFoundCount' => \count($notFoundIds),
'notFoundIds' => $notFoundIds,
'requestedCount' => \count($crmIds),
'fetchedCount' => \count($allContacts),
]);
}
if (empty($allContacts)) {
return $result;
}
$prepareStart = microtime(true);
$accountMappings = $this->prepareAccountMappingsForContacts($allContacts);
$prepareAccountsMs = (int) round((microtime(true) - $prepareStart) * 1000);
$loopStart = microtime(true);
foreach ($allContacts as $contactData) {
$contactStart = microtime(true);
try {
$contact = $this->importContact($contactData, $accountMappings);
if ($contact !== null) {
$result['success_count']++;
}
} catch (\Throwable $e) {
$crmId = $contactData['id'] ?? 'unknown';
$result['failed_ids'][] = $crmId;
$result['errors'][$crmId] = $e->getMessage();
$this->logger->warning('[' . $this->getDisplayName() . '] Failed to import contact', [
'teamId' => $this->team->getId(),
'crmId' => $crmId,
'error' => $e->getMessage(),
]);
}
$contactMs = (int) round((microtime(true) - $contactStart) * 1000);
if ($contactMs > 1000) {
$slowContacts[] = ['crmId' => $contactData['id'] ?? 'unknown', 'ms' => $contactMs];
}
}
$loopMs = (int) round((microtime(true) - $loopStart) * 1000);
$totalMs = (int) round((microtime(true) - $batchStart) * 1000);
$this->logger->info('[' . $this->getDisplayName() . '] importContactBatch timing', [
'teamId' => $this->team->getId(),
'contact_count' => \count($allContacts),
'requested_count' => \count($crmIds),
'not_found_count' => \count($notFoundIds),
'total_ms' => $totalMs,
'fetch_api_ms' => $fetchMs,
'prepare_accounts_ms' => $prepareAccountsMs,
'contacts_loop_ms' => $loopMs,
'avg_contact_ms' => ! empty($allContacts) ? (int) round($loopMs / \count($allContacts)) : 0,
'slow_contacts_count' => \count($slowContacts),
'slow_contacts' => array_slice($slowContacts, 0, 10),
]);
return $result;
}
private function fetchContactsByIdsInChunks(array $crmIds): array
{
$fields = $this->getContactFields();
$allContacts = [];
foreach (array_chunk($crmIds, self::BATCH_SIZE) as $chunk) {
try {
$contacts = $this->client->getContactsByIds($chunk, $fields);
foreach ($contacts as $contactData) {
$allContacts[] = $contactData;
}
} catch (\Throwable $e) {
// @TODO what will happen if this exception is thrown
$this->logger->warning('[' . $this->getDisplayName() . '] Batch contact fetch failed', [
'chunk_size' => \count($chunk),
'error' => $e->getMessage(),
]);
throw $e;
}
}
return $allContacts;
}
private function prepareAccountMappingsForContacts(array $contacts): array
{
$companyIds = [];
foreach ($contacts as $contact) {
$companyId = $contact['properties']['associatedcompanyid'] ?? null;
if ($companyId !== null && $companyId !== '') {
$companyIds[] = (string) $companyId;
}
}
$companyIds = array_unique($companyIds);
if (empty($companyIds)) {
return [];
}
$mappings = $this->crmEntityRepository
->getExistingAccountIdsMap($this->config, $companyIds);
$missingCompanyIds = array_diff($companyIds, array_keys($mappings));
if (empty($missingCompanyIds)) {
return $mappings;
}
$this->logger->info('[' . $this->getDisplayName() . '] Batch syncing missing accounts for contacts', [
'teamId' => $this->team->getId(),
'total_companies' => \count($companyIds),
'existing_companies' => \count($mappings),
'missing_companies' => \count($missingCompanyIds),
]);
try {
$syncedAccounts = $this->batchSyncAccountsForContacts($missingCompanyIds);
$mappings = array_merge($mappings, $syncedAccounts);
} catch (\Throwable $e) {
$this->logger->error('[' . $this->getDisplayName() . '] Failed to batch sync missing accounts', [
'teamId' => $this->team->getId(),
'missingCompanyIds' => $missingCompanyIds,
'missingCount' => count($missingCompanyIds),
'error' => $e->getMessage(),
]);
}
return $mappings;
}
private function batchSyncAccountsForContacts(array $companyIds): array
{
$syncedAccounts = [];
$fields = $this->getCompanyFields();
foreach (array_chunk($companyIds, self::BATCH_SIZE) as $chunk) {
try {
$companies = $this->client->getCompaniesByIds($chunk, $fields);
foreach ($companies as $companyData) {
try {
$account = $this->importAccount($companyData);
if ($account) {
$syncedAccounts[$account->getCrmProviderId()] = $account->getId();
}
} catch (\Throwable $e) {
$this->logger->warning('[' . $this->getDisplayName() . '] Failed to import account in batch', [
'teamId' => $this->team->getId(),
'companyId' => $companyData['id'] ?? 'unknown',
'error' => $e->getMessage(),
]);
}
}
} catch (\Throwable $e) {
$this->logger->warning('[' . $this->getDisplayName() . '] Batch company fetch failed', [
'teamId' => $this->team->getId(),
'chunk_size' => \count($chunk),
'error' => $e->getMessage(),
]);
}
}
return $syncedAccounts;
}
/**
* Process webhook-collected company batches.
*
* Drains Redis sets containing company CRM IDs collected from webhook events
* and dispatches ImportAccountBatch jobs for batch processing.
*
* @return int Number of company IDs dispatched to jobs
*/
public function batchSyncCompanies(): int
{
$configId = $this->team->getCrmConfiguration()->getId();
return $this->batchProcessor->processBatchesForObjectType(
WebhookSyncBatchProcessor::OBJECT_TYPE_COMPANY,
$configId
);
}
public function importAccountBatchByIds(array $crmIds): array
{
$result = [
'success_count' => 0,
'failed_ids' => [],
'errors' => [],
];
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$batchStart = microtime(true);
$slowAccounts = [];
$fields = $this->getCompanyFields();
$allCompanies = [];
$fetchStart = microtime(true);
foreach (array_chunk($crmIds, self::BATCH_SIZE) as $chunk) {
try {
$companies = $this->client->getCompaniesByIds($chunk, $fields);
foreach ($companies as $companyData) {
$allCompanies[] = $companyData;
}
} catch (\Throwable $e) {
$this->logger->warning('[' . $this->getDisplayName() . '] Batch company fetch failed', [
'chunk_size' => \count($chunk),
'error' => $e->getMessage(),
]);
throw $e;
}
}
$fetchMs = (int) round((microtime(true) - $fetchStart) * 1000);
$fetchedIds = array_map('strval', array_column($allCompanies, 'id'));
$notFoundIds = array_values(array_diff(array_map('strval', $crmIds), $fetchedIds));
if (! empty($notFoundIds)) {
$this->logger->info('[' . $this->getDisplayName() . '] Company CRM IDs not found in HubSpot', [
'teamId' => $this->team->getId(),
'notFoundCount' => \count($notFoundIds),
'notFoundIds' => $notFoundIds,
'requestedCount' => \count($crmIds),
'fetchedCount' => \count($allCompanies),
]);
}
$loopStart = microtime(true);
foreach ($allCompanies as $companyData) {
$accountStart = microtime(true);
try {
$account = $this->importAccount($companyData);
if ($account !== null) {
$result['success_count']++;
}
} catch (\Throwable $e) {
$crmId = $companyData['id'] ?? $companyData['properties']['hs_object_id'] ?? 'unknown';
$result['failed_ids'][] = $crmId;
$result['errors'][$crmId] = $e->getMessage();
$this->logger->warning('[' . $this->getDisplayName() . '] Failed to import account', [
'crmId' => $crmId,
'error' => $e->getMessage(),
]);
}
$accountMs = (int) round((microtime(true) - $accountStart) * 1000);
if ($accountMs > 1000) {
$crmId = $companyData['id'] ?? $companyData['properties']['hs_object_id'] ?? 'unknown';
$slowAccounts[] = ['crmId' => $crmId, 'ms' => $accountMs];
}
}
$loopMs = (int) round((microtime(true) - $loopStart) * 1000);
$totalMs = (int) round((microtime(true) - $batchStart) * 1000);
$this->logger->info('[' . $this->getDisplayName() . '] importAccountBatch timing', [
'teamId' => $this->team->getId(),
'account_count' => \count($allCompanies),
'requested_count' => \count($crmIds),
'not_found_count' => \count($notFoundIds),
'total_ms' => $totalMs,
'fetch_api_ms' => $fetchMs,
'accounts_loop_ms' => $loopMs,
'avg_account_ms' => ! empty($allCompanies) ? (int) round($loopMs / \count($allCompanies)) : 0,
'slow_accounts_count' => \count($slowAccounts),
'slow_accounts' => array_slice($slowAccounts, 0, 10),
]);
return $result;
}
private function getCompanyFields(): array
{
return [
'country',
'name',
'phone',
'domain',
'hs_avatar_filemanager_key',
'industry',
'hubspot_owner_id',
'hs_object_id',
'createdate',
'hs_lastmodifieddate',
];
}
private function importAccount($crmData): ?Account
{
$crmProviderId = $crmData['id'] ?? $crmData['properties']['hs_object_id'] ?? null;
$this->logger->info('[HubSpot] importAccount', [
'crm_provider_id' => $crmProviderId,
'config_id' => $this->config->getId(),
]);
if (empty($crmData['properties'])) {
$this->logger->info('[HubSpot] importAccount failed: empty properties', [
'crm_provider_id' => $crmProviderId,
'config_id' => $this->config->getId(),
]);
return null;
}
$properties = $crmData['properties'];
$crmId = (string) $properties['hs_object_id'];
$countryCode = isset($properties['country']) ? $this->convertCountryNameToCode($properties['country']) : null;
if (isset($properties['phone'])) {
// Trim to our width and attempt to parse it.
$number = mb_strimwidth($properties['phone'], 0, 25);
$parsedNumber = parsePhoneNumber($countryCode, $number);
} else {
$parsedNumber = [];
}
$name = '[unknown]';
if (isset($properties['name'])) {
$name = $properties['name'];
}
$photoPath = $this->prospectPhotoPathService->getOrGeneratePhotoPath(
$this->config,
$crmId,
Account::class,
$crmId,
$name
);
$industry = null;
if (isset($properties['industry'])) {
$industry = mb_strimwidth($properties['industry'], 0, 40);
}
$ownerId = $profile = null;
if (isset($properties['hubspot_owner_id'])) {
$ownerId = $properties['hubspot_owner_id'];
$profile = $this->crmEntityRepository->findProfileByExternalId($this->config, (string) $ownerId);
}
$domain = null;
if (isset($properties['domain'])) {
$domain = StringUtil::resolveDomain($properties['domain']);
}
$remotelyCreatedAt = null;
if (isset($properties['createdate']) && ! empty($properties['createdate'])) {
$remotelyCreatedAt = Carbon::parse($properties['createdate']);
}
$data = [
'crm_provider_id' => $crmId,
'team_id' => $this->team->id,
'user_id' => $profile ? $profile->user_id : null,
'owner_id' => $ownerId,
'name' => mb_strimwidth($name, 0, 191),
'photo_path' => $photoPath,
'industry' => $industry,
'domain' => $domain !== null
? substr($domain, 0, 191)
: null,
'phone' => $parsedNumber['phone'] ?? null,
'ext' => $parsedNumber['ext'] ?? null,
'country_code' => $countryCode,
'remotely_created_at' => $remotelyCreatedAt,
];
return $this->crmEntityRepository->importAccount($this->config, $data);
}
public function deleteContact(string $crmProviderId): bool
{
try {
$contact = $this->crmEntityRepository->findContactByExternalId($this->config, $crmProviderId);
if (! $contact) {
$this->logger->info('[HubSpot] Contact not found for deletion', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
// return success because we do not know which instance is the target
return true;
}
$id = $contact->getId();
$this->logger->info('[HubSpot] Deleting contact via webhook', [
'contact_id' => $id,
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
$contact->delete();
DeleteContactJob::dispatch($id)->afterCommit();
return true;
} catch (Exception $e) {
$this->logger->error('[HubSpot] Failed to delete contact via webhook', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
'error' => $e->getMessage(),
]);
return false;
}
}
public function deleteAccount(string $crmProviderId): bool
{
try {
$account = $this->crmEntityRepository->findAccountByExternalId($this->config, $crmProviderId);
if (! $account) {
$this->logger->info('[HubSpot] Account not found for deletion', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
// return success because we do not know which instance is the target
return true;
}
$id = $account->getId();
$this->logger->info('[HubSpot] Deleting account via webhook', [
'account_id' => $id,
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
$account->delete();
DeleteAccountJob::dispatch($id)->afterCommit();
return true;
} catch (Exception $e) {
$this->logger->error('[HubSpot] Failed to delete account via webhook', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
'error' => $e->getMessage(),
]);
return false;
}
}
public function deleteOpportunity(string $crmProviderId): bool
{
try {
$opportunity = $this->crmEntityRepository->findOpportunityByExternalId($this->config, $crmProviderId);
if (! $opportunity) {
$this->logger->info('[HubSpot] Opportunity not found for deletion', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
// return success because we do not know which instance is the target
return true;
}
$id = $opportunity->getId();
$this->logger->info('[HubSpot] Deleting opportunity via webhook', [
'opportunity_id' => $id,
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
$opportunity->delete();
DeleteOpportunityJob::dispatch($id)->afterCommit();
return true;
} catch (Exception $e) {
$this->logger->error('[HubSpot] Failed to delete opportunity via webhook', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
'error' => $e->getMessage(),
]);
return false;
}
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide
app ~/jiminny/app, folder
.circleci, folder
.cursor, folder
.github
.sonarlint, folder
.vscode, folder
.windsurf, folder
app, sources root
Actions, folder
Component, folder
Acl, folder
ActionItems, folder
Activity, folder
ActivityAnalytics, folder
ActivitySearch, folder
AiActivityType, folder
AiAutomation, folder
AiCallScoring, folder
AskAnything, folder
Dtos, folder
Events, folder
AskAnythingPromptService.php
HistoryService.php
AskJiminnyAi, folder
AWS, folder
BillingManagement, folder
Cache, folder
CoachingFeedback, folder
Country, folder
CustomerApi, folder
Database, folder
Datadog, folder
DateTime, folder
DealInsights, folder
DealRisks, folder
ElasticSearch
Eloquent
Encoding
Encryption
ES
Faker
FeatureFlags
FFMpeg
FileSystem
Gecko
Gong
GuzzleHttp
KeyPoints
Kiosk
LanguageDetection
LiveFeed
Locks
Math
MediaPipeline
MeetingBot, folder
MobileSettings, folder
Model, folder
Notification, folder
Nudge, folder
ParagraphBreaker, folder
ParticipantSpeech, folder
PartitionedCookie, folder
PlaybackPage, folder
Playlist, folder
Prophet, folder
ProphetAi, folder
ProsperWorks, folder
Queue, folder
Job, folder
RateLimitAware.php, abstract class
RateLimitAwareWrapper.php, class
BotsQueueConstants.php
Constants.php
ProcessingQueueConstants.php
Router, folder
Saml2, folder
SCIM, folder
Seeder, folder
Sentry, folder
Serializer, folder
Settings, folder
Sidekick, folder
Slack, folder
TeamInsights, folder
TimeMemoryMapper, folder
Transcription, folder
TranscriptionSummary, folder
Twilio, folder
Uploader, folder
UrlGenerator, folder
Utility, folder
Exceptions, folder
Service, folder
BaseRateLimiter.php, class
EfficientJsonParser.php, class
ProviderRateLimiter.php, class
RateLimiterInstance.php, class
Uuid, folder
Waveform, folder
Webhooks, folder
Workflow, folder
Configuration, folder
Console, folder
Commands, folder
Activities, folder
Analytics, folder
Calendars, folder
Crm, folder
Hubspot, folder
IntegrationApp, folder
Traits, folder
AddLayoutEntities.php, class
AutologDelayedCommand.php, class
BullhornCommandAbstract.php, abstract class
BullhornPingCommand.php, class
BullhornSearchCommand.php, class
BullhornSessionCommand.php, class
CheckActivityLoggableCommand.php, final class
CleanDuplicateFieldDataCommand.php, class
FullSyncOpportunityCommand.php, class
LogActivitiesCommand.php, final class
ManageSyncStrategyCommand.php, class
MatchCrmObjectsCommand.php, class
MatchOpportunityActivitiesCommand.php, class
MigrateProvider.php, class
ProcessHubspotObjectsSyncBatches.php, class
PurgeDeletedOpportunitiesCommand.php, class
ResetGovernorLimits.php, class
SendNotLogged.php, class
SetupActivityTypeForFollowUp.php, final class
SetupCloseCrm.php, class
SetupCopperCrm.php, class
SetupCrmCommand.php, abstract class
SetupLayouts.php, class
SyncAccount.php, class
SyncContact.php, class
SyncFieldMetadata.php, class
SyncHubspotActiveDeals.php, class
SyncHubspotObjects.php, class
SyncLead.php, class
SyncObjects.php, class
SyncOpportunitiesMissingFieldDataCommand.php, class
SyncOpportunity.php, class
SyncProfileMetadata.php, class
SyncTeamMetadata.php, class
UpdateOpportunitySpecifications.php, class
DealInsights, folder
Dev, folder
AddRateLimitCommand.php, class
FixHubSpotTokens.php, class
FixMissMatchedCrmActivitiesCommand.php, final class
ImportCallsCommand.php, class
MonitorSocialAccountsState.php, class
Dialers, folder
DTOs, folder
Elasticsearch, folder
EngagementStats, folder
GeckoExport, folder
Livestream, folder
Mailboxes, folder
Migrate, folder
PlaybackThemes, folder
Playbooks, folder
Playlists, folder
Postmark, folder
ProphetAi, folder
Reports, folder
AutomatedReportsCommand.php, class
AutomatedReportsRetentionPolicyCommand.php, class
AutomatedReportsSendCommand.php, class
CreateMockAskJiminnyReportResultCommand.php, class
DeleteReportCommand.php, class
GenerateMarketingReport.php, class
Team.php, class
Usage.php, class
Slack, folder
Teams, folder
Tracks, folder
Transcription, folder
Twilio, folder
Users, folder
Vocabulary, folder
Zoom, folder
Command.php
CreateDatabaseUsers.php
DatabaseTableCount.php
DeleteOldAiCrmNotesCommand.php
DeleteS3LeftoversCommand.php
DevPostmanCommand.php
DiarizeViaAiParticipantIdentificationCommand.php
EncryptTokensCommand.php
EngagementStatsRegenerateCommand.php
FeatureFlagsHelper.php
FixCrossTenantIssues.php
FlushRolesPermissionsCache.php
GenerateInternalWebhookToken.php
GroupSetDefaultLanguageCommand.php
HelperTruncateCoachingTables.php
HubspotJournalPollingCommand.php
HubspotWebhookServiceCommand.php
ImportRecording.php
ImportUsersFromCsvFile.php
IterateUsersCommand.php
JiminnyCacheClearCommand.php
JiminnyDebugCommand.php
JiminnySetEncryptedTokenManagerModeCommand.php
JiminnyTokenInfoCommand.php
MakeSlackLiveCoachingChatNotesOn.php
ManageScimForTeam.php
MarkBranchForEnvironmentPipelineCommand.php
MuteOrganizerChannel.php, class
PhpApm.php, class
PropagateCoachingFeedbackCreatedAtToSectionFeedbacks.php, class
PurgeConferences.php, class
PurgeSoftDeletedOpportunitiesCommand.php, class
PurgeSyncBatchesCommand.php, class
RecalculateDealRisksCommand.php, class
RemoveDeleteMarkersCommand.php
RemoveExpiredNudgesCommand.php
RemoveUnusedParticipantSpeechesCommand.php
ResetElasticSearch.php
RestoreActivityCrmProviderIdCommand.php
RestoreActivityTypeCommand.php
RunAiCallScoringForUntypedActivitiesCommand.php
SeedActivities.php
SendNudgeExpirationWarningsCommand.php
SyncActivity.php, class...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"on_screen":true,"help_text":"Git Branch: master","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"Editor for custom.log","depth":4,"on_screen":true,"role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Analyzing…","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits;\n\nuse Carbon\\Carbon;\nuse Exception;\nuse Illuminate\\Support\\Str;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Jobs\\Crm\\Delete\\DeleteAccountJob;\nuse Jiminny\\Jobs\\Crm\\Delete\\DeleteContactJob;\nuse Jiminny\\Jobs\\Crm\\Delete\\DeleteOpportunityJob;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Repositories\\Crm\\CrmEntityRepository;\nuse Jiminny\\Services\\Avatar\\ProspectPhotoPathService;\nuse Jiminny\\Services\\Crm\\Hubspot\\HubspotClientInterface;\nuse Jiminny\\Services\\Crm\\Hubspot\\WebhookSyncBatchProcessor;\nuse Jiminny\\Utils\\StringUtil;\n\ntrait SyncCrmEntitiesTrait\n{\n use OpportunitySyncTrait;\n private const string CDN_URL = 'https://cdn2.hubspot.net/';\n protected CrmEntityRepository $crmEntityRepository;\n protected ProspectPhotoPathService $prospectPhotoPathService;\n\n private function getAssociationDataForCollection(array $collection, string $fromObject, string $toObject): array\n {\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $hsOpportunityIds = array_column($collection, 'id');\n\n return $this->client->getAssociationsData($hsOpportunityIds, $fromObject, $toObject);\n }\n\n private function importAssociationData(array $collection, array $associatedData): array\n {\n $data = [];\n if (! empty($associatedData[$collection['id']])) {\n foreach ($associatedData[$collection['id']] as $id) {\n $data[] = [\n 'id' => $id,\n ];\n }\n }\n\n return ['results' => $data];\n }\n\n /**\n * Sync contacts modified since a given date (manual sync mode).\n *\n * This method fetches contacts from HubSpot API based on modification date and\n * imports them one by one. It is used for:\n * - Manual sync commands (e.g., crm:sync-contact with --from parameter)\n * - Initial sync for new teams\n * - Backfill operations\n *\n * For regular sync webhook batchSyncContacts is used:\n *\n * @param Carbon $since Fetch contacts modified after this date\n * @param Carbon|null $to Optional end date for modification range\n *\n * @return int Number of contacts successfully synced\n */\n public function syncContacts(Carbon $since, ?Carbon $to = null): int\n {\n $syncCount = 0;\n\n try {\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $fields = $this->getContactFields();\n $payload = $this->payloadBuilder->getRecentlyUpdatedSearchPayload($since, $to, $fields);\n\n foreach ($this->client->getPaginatedDataGenerator($payload, 'contacts') as $hsContact) {\n $this->importContact($hsContact);\n $syncCount++;\n }\n } catch (Exception $exception) {\n $this->logger->error('[' . $this->getDisplayName() . '] Sync contacts failed', [\n 'teamId' => $this->team->getUuid(),\n 'reason' => $exception->getMessage(),\n ]);\n }\n\n return $syncCount;\n }\n\n /**\n * @inheritdoc\n */\n public function syncContact(string $crmId): ?Contact\n {\n try {\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $fields = $this->getContactFields();\n $hsContact = $this->client->getContactById($crmId, $fields);\n } catch (\\HubSpot\\Client\\Crm\\Contacts\\ApiException $e) {\n $this->logger->info('[' . $this->getDisplayName() . '] Contact fetch failed', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n return null;\n } catch (CrmException $e) {\n $this->logger->info('[' . $this->getDisplayName() . '] Contact not found', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n return null;\n }\n\n if (empty($hsContact['properties']) || empty($hsContact['id'])) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Contact data incomplete', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'has_properties' => ! empty($hsContact['properties']),\n 'has_id' => ! empty($hsContact['id']),\n ]);\n\n return null;\n }\n\n return $this->importContact($hsContact);\n }\n\n private function getContactFields(): array\n {\n return [\n 'associatedcompanyid',\n 'country',\n 'firstname',\n 'lastname',\n 'phone',\n 'mobilephone',\n 'email',\n 'photo',\n 'hs_avatar_filemanager_key',\n 'jobtitle',\n 'hubspot_owner_id',\n 'hs_object_id',\n 'createdate',\n 'hs_lastmodifieddate',\n ];\n }\n\n /**\n * @inheritdoc\n */\n private function importContact($crmData, array $accountMappings = []): ?Contact\n {\n $crmProviderId = $crmData['id'] ?? null;\n\n $this->logger->info('[HubSpot] importContact', [\n 'crm_provider_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n if (empty($crmData['properties'])) {\n $this->logger->info('[HubSpot] importContact failed: empty properties', [\n 'crm_provider_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n return null;\n }\n\n $properties = $crmData['properties'];\n $crmId = (string) $crmData['id'];\n\n $accountId = $this->resolveContactAccount($properties, $accountMappings);\n $data = $this->buildContactData($crmId, $properties, $accountId);\n\n return $this->crmEntityRepository->importContact($this->config, $data);\n }\n\n private function resolveContactAccount(array $properties, array $accountMappings): ?int\n {\n if (empty($properties['associatedcompanyid'])) {\n return null;\n }\n\n $companyId = (string) $properties['associatedcompanyid'];\n\n if (! empty($accountMappings)) {\n return $accountMappings[$companyId] ?? null;\n }\n\n return $this->crmEntityRepository->findAccountByExternalId(\n $this->team->getCrmConfiguration(),\n $companyId\n )?->getId() ?? $this->syncAccount($companyId)?->getId();\n }\n\n private function buildContactData(string $crmId, array $properties, ?int $accountId): array\n {\n $countryCode = $this->buildContactCountry($properties);\n $name = $this->buildContactName($properties);\n $photoPath = $this->teamService->generateAvatar(\n $crmId,\n empty($name) ? ($properties['email'] ?? 'N/A') : $name,\n );\n $parsedNumber = $this->buildContactPhone($countryCode, $properties);\n $mobileNumber = $this->buildContactMobilePhone($countryCode, $properties);\n\n $ownerId = $properties['hubspot_owner_id'] ?? null;\n $profile = $ownerId !== null\n ? $this->crmEntityRepository->findProfileByExternalId($this->config, (string) $ownerId)\n : null;\n\n $ext = (isset($parsedNumber['ext']) && is_string($parsedNumber['ext']) && strlen($parsedNumber['ext']) <= 10)\n ? $parsedNumber['ext']\n : null;\n\n $title = isset($properties['jobtitle']) ? mb_strimwidth($properties['jobtitle'], 0, 128) : null;\n $email = isset($properties['email']) ? mb_strimwidth($properties['email'], 0, 191) : null;\n $remotelyCreatedAt = ! empty($properties['createdate']) ? Carbon::parse($properties['createdate']) : null;\n\n return [\n 'crm_provider_id' => $crmId,\n 'team_id' => $this->team->getId(),\n 'account_id' => $accountId,\n 'user_id' => $profile?->getUserId(),\n 'owner_id' => $ownerId,\n 'name' => $name,\n 'title' => $title,\n 'email' => $email,\n 'country_code' => $countryCode,\n 'phone' => $parsedNumber['phone'] ?? null,\n 'mobile_phone' => $mobileNumber ?? null,\n 'ext' => $ext,\n 'photo_path' => $photoPath,\n 'remotely_created_at' => $remotelyCreatedAt,\n ];\n }\n\n /**\n * @param $properties\n */\n private function buildContactName($properties): string\n {\n if (is_array($properties)) {\n return $this->buildContactNameFromArray($properties);\n }\n\n return $this->buildContactNameFromObject($properties);\n }\n\n private function buildContactNameFromArray(array $properties): string\n {\n if (! empty($properties['name'])) {\n return mb_strimwidth($properties['name'], 0, 100);\n }\n\n $name = '';\n if (! empty($properties['firstname'])) {\n $name = $properties['firstname'] . ' ';\n }\n\n if (! empty($properties['lastname'])) {\n $name .= $properties['lastname'];\n }\n\n if ($name === '' && ! empty($properties['email'])) {\n $name = $properties['email'];\n }\n\n return mb_strimwidth($name, 0, 100);\n }\n\n private function buildContactNameFromObject($properties): string\n {\n $name = '';\n if (isset($properties->firstname)) {\n $name = $properties->firstname->value . ' ';\n }\n if (isset($properties->lastname)) {\n $name .= $properties->lastname->value;\n }\n if ($name === '' && isset($properties->email)) {\n $name = $properties->email->value;\n }\n\n return mb_strimwidth($name, 0, 100);\n }\n\n /**\n * @param $properties\n */\n private function buildContactPhone(?string $countryCode, $properties): ?array\n {\n if (is_array($properties) && empty($properties['phone']) === false) {\n $number = mb_strimwidth($properties['phone'], 0, 25);\n\n return parsePhoneNumber($countryCode, $number);\n } elseif (isset($properties->phone)) {\n $number = mb_strimwidth($properties->phone->value, 0, 25);\n\n return parsePhoneNumber($countryCode, $number);\n }\n\n return [];\n }\n\n /**\n * @param $properties\n */\n private function buildContactMobilePhone(?string $countryCode, $properties): ?string\n {\n return isset($properties['mobilephone'])\n ? Str::limit(phone_e164($countryCode, $properties['mobilephone']), 25, '')\n : null;\n }\n\n /**\n * @param $properties\n * @param $account\n */\n private function buildContactCountry($properties): ?string\n {\n if (is_array($properties) && empty($properties['country']) === false) {\n return $this->convertCountryNameToCode($properties['country']);\n }\n\n if (isset($properties->country)) {\n return $this->convertCountryNameToCode($properties->country->value);\n }\n\n return null;\n }\n\n /**\n * HubSpot doesn't have leads, so this method does nothing.\n *\n * @param Carbon $since\n * @param Carbon|null $to\n * @param string|null $crmProfileId\n *\n * @return int\n */\n public function syncLeads(Carbon $since, ?Carbon $to = null, ?string $crmProfileId = null): int\n {\n // Mark unused parameters to avoid code smell warnings\n unset($since, $to, $crmProfileId);\n\n return 0;\n }\n\n /**\n * HubSpot doesn't have leads.\n *\n * @param string $crmId\n *\n * @inheritdoc\n */\n public function syncLead(string $crmId): ?Lead\n {\n // Mark unused parameter to avoid code smell warnings\n unset($crmId);\n\n return null;\n }\n\n /**\n * Sync accounts (companies) modified since a given date (manual sync mode).\n *\n * This method fetches companies from HubSpot API based on modification date and\n * imports them one by one. It is used for:\n * - Manual sync commands (e.g., crm:sync-account with --from parameter)\n * - Initial sync for new teams\n * - Backfill operations\n *\n * For regular sync webhook batchSyncCompanies is used:\n *\n * @param Carbon $since Fetch companies modified after this date\n * @param Carbon|null $to Optional end date for modification range\n *\n * @return int Number of companies successfully synced\n */\n public function syncAccounts(Carbon $since, ?Carbon $to = null): int\n {\n $syncCount = 0;\n\n try {\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $fields = $this->getCompanyFields();\n $payload = $this->payloadBuilder->getRecentlyUpdatedSearchPayload($since, $to, $fields);\n\n foreach ($this->client->getPaginatedDataGenerator($payload, 'companies') as $hsAccount) {\n $this->importAccount($hsAccount);\n $syncCount++;\n }\n } catch (Exception $exception) {\n $this->logger->error('[' . $this->getDisplayName() . '] Sync accounts failed', [\n 'teamId' => $this->team->getUuid(),\n 'reason' => $exception->getMessage(),\n ]);\n }\n\n return $syncCount;\n }\n\n /**\n * @inheritdoc\n */\n public function syncAccount(string $crmId): ?Account\n {\n try {\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $fields = $this->getCompanyFields();\n $hsAccount = $this->client->getAccountById($crmId, $fields);\n } catch (\\HubSpot\\Client\\Crm\\Companies\\ApiException $e) {\n $this->logger->info('[' . $this->getDisplayName() . '] Account fetch failed', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n return null;\n } catch (CrmException $e) {\n $this->logger->info('[' . $this->getDisplayName() . '] Account not found', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n return null;\n }\n\n return $this->importAccount($hsAccount);\n }\n\n /**\n * Process webhook-collected contact batches.\n *\n * Drains Redis sets containing contact CRM IDs collected from webhook events\n * and dispatches ImportContactBatch jobs for batch processing.\n *\n * @return int Number of contact IDs dispatched to jobs\n */\n public function batchSyncContacts(): int\n {\n $configId = $this->team->getCrmConfiguration()->getId();\n\n return $this->batchProcessor->processBatchesForObjectType(\n WebhookSyncBatchProcessor::OBJECT_TYPE_CONTACT,\n $configId\n );\n }\n\n public function importContactBatchByIds(array $crmIds): array\n {\n $result = [\n 'success_count' => 0,\n 'failed_ids' => [],\n 'errors' => [],\n ];\n\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $batchStart = microtime(true);\n $slowContacts = [];\n\n $fetchStart = microtime(true);\n $allContacts = $this->fetchContactsByIdsInChunks($crmIds);\n $fetchMs = (int) round((microtime(true) - $fetchStart) * 1000);\n\n $fetchedIds = array_map('strval', array_column($allContacts, 'id'));\n $notFoundIds = array_values(array_diff(array_map('strval', $crmIds), $fetchedIds));\n\n if (! empty($notFoundIds)) {\n $this->logger->info('[' . $this->getDisplayName() . '] Contact CRM IDs not found in HubSpot', [\n 'teamId' => $this->team->getId(),\n 'notFoundCount' => \\count($notFoundIds),\n 'notFoundIds' => $notFoundIds,\n 'requestedCount' => \\count($crmIds),\n 'fetchedCount' => \\count($allContacts),\n ]);\n }\n\n if (empty($allContacts)) {\n return $result;\n }\n\n $prepareStart = microtime(true);\n $accountMappings = $this->prepareAccountMappingsForContacts($allContacts);\n $prepareAccountsMs = (int) round((microtime(true) - $prepareStart) * 1000);\n\n $loopStart = microtime(true);\n foreach ($allContacts as $contactData) {\n $contactStart = microtime(true);\n\n try {\n $contact = $this->importContact($contactData, $accountMappings);\n if ($contact !== null) {\n $result['success_count']++;\n }\n } catch (\\Throwable $e) {\n $crmId = $contactData['id'] ?? 'unknown';\n $result['failed_ids'][] = $crmId;\n $result['errors'][$crmId] = $e->getMessage();\n\n $this->logger->warning('[' . $this->getDisplayName() . '] Failed to import contact', [\n 'teamId' => $this->team->getId(),\n 'crmId' => $crmId,\n 'error' => $e->getMessage(),\n ]);\n }\n\n $contactMs = (int) round((microtime(true) - $contactStart) * 1000);\n if ($contactMs > 1000) {\n $slowContacts[] = ['crmId' => $contactData['id'] ?? 'unknown', 'ms' => $contactMs];\n }\n }\n $loopMs = (int) round((microtime(true) - $loopStart) * 1000);\n $totalMs = (int) round((microtime(true) - $batchStart) * 1000);\n\n $this->logger->info('[' . $this->getDisplayName() . '] importContactBatch timing', [\n 'teamId' => $this->team->getId(),\n 'contact_count' => \\count($allContacts),\n 'requested_count' => \\count($crmIds),\n 'not_found_count' => \\count($notFoundIds),\n 'total_ms' => $totalMs,\n 'fetch_api_ms' => $fetchMs,\n 'prepare_accounts_ms' => $prepareAccountsMs,\n 'contacts_loop_ms' => $loopMs,\n 'avg_contact_ms' => ! empty($allContacts) ? (int) round($loopMs / \\count($allContacts)) : 0,\n 'slow_contacts_count' => \\count($slowContacts),\n 'slow_contacts' => array_slice($slowContacts, 0, 10),\n ]);\n\n return $result;\n }\n\n private function fetchContactsByIdsInChunks(array $crmIds): array\n {\n $fields = $this->getContactFields();\n $allContacts = [];\n\n foreach (array_chunk($crmIds, self::BATCH_SIZE) as $chunk) {\n try {\n $contacts = $this->client->getContactsByIds($chunk, $fields);\n foreach ($contacts as $contactData) {\n $allContacts[] = $contactData;\n }\n } catch (\\Throwable $e) {\n // @TODO what will happen if this exception is thrown\n $this->logger->warning('[' . $this->getDisplayName() . '] Batch contact fetch failed', [\n 'chunk_size' => \\count($chunk),\n 'error' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n }\n\n return $allContacts;\n }\n\n private function prepareAccountMappingsForContacts(array $contacts): array\n {\n $companyIds = [];\n foreach ($contacts as $contact) {\n $companyId = $contact['properties']['associatedcompanyid'] ?? null;\n if ($companyId !== null && $companyId !== '') {\n $companyIds[] = (string) $companyId;\n }\n }\n\n $companyIds = array_unique($companyIds);\n\n if (empty($companyIds)) {\n return [];\n }\n\n $mappings = $this->crmEntityRepository\n ->getExistingAccountIdsMap($this->config, $companyIds);\n\n $missingCompanyIds = array_diff($companyIds, array_keys($mappings));\n\n if (empty($missingCompanyIds)) {\n return $mappings;\n }\n\n $this->logger->info('[' . $this->getDisplayName() . '] Batch syncing missing accounts for contacts', [\n 'teamId' => $this->team->getId(),\n 'total_companies' => \\count($companyIds),\n 'existing_companies' => \\count($mappings),\n 'missing_companies' => \\count($missingCompanyIds),\n ]);\n\n try {\n $syncedAccounts = $this->batchSyncAccountsForContacts($missingCompanyIds);\n $mappings = array_merge($mappings, $syncedAccounts);\n } catch (\\Throwable $e) {\n $this->logger->error('[' . $this->getDisplayName() . '] Failed to batch sync missing accounts', [\n 'teamId' => $this->team->getId(),\n 'missingCompanyIds' => $missingCompanyIds,\n 'missingCount' => count($missingCompanyIds),\n 'error' => $e->getMessage(),\n ]);\n }\n\n return $mappings;\n }\n\n private function batchSyncAccountsForContacts(array $companyIds): array\n {\n $syncedAccounts = [];\n $fields = $this->getCompanyFields();\n\n foreach (array_chunk($companyIds, self::BATCH_SIZE) as $chunk) {\n try {\n $companies = $this->client->getCompaniesByIds($chunk, $fields);\n\n foreach ($companies as $companyData) {\n try {\n $account = $this->importAccount($companyData);\n if ($account) {\n $syncedAccounts[$account->getCrmProviderId()] = $account->getId();\n }\n } catch (\\Throwable $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Failed to import account in batch', [\n 'teamId' => $this->team->getId(),\n 'companyId' => $companyData['id'] ?? 'unknown',\n 'error' => $e->getMessage(),\n ]);\n }\n }\n } catch (\\Throwable $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Batch company fetch failed', [\n 'teamId' => $this->team->getId(),\n 'chunk_size' => \\count($chunk),\n 'error' => $e->getMessage(),\n ]);\n }\n }\n\n return $syncedAccounts;\n }\n\n /**\n * Process webhook-collected company batches.\n *\n * Drains Redis sets containing company CRM IDs collected from webhook events\n * and dispatches ImportAccountBatch jobs for batch processing.\n *\n * @return int Number of company IDs dispatched to jobs\n */\n public function batchSyncCompanies(): int\n {\n $configId = $this->team->getCrmConfiguration()->getId();\n\n return $this->batchProcessor->processBatchesForObjectType(\n WebhookSyncBatchProcessor::OBJECT_TYPE_COMPANY,\n $configId\n );\n }\n\n public function importAccountBatchByIds(array $crmIds): array\n {\n $result = [\n 'success_count' => 0,\n 'failed_ids' => [],\n 'errors' => [],\n ];\n\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $batchStart = microtime(true);\n $slowAccounts = [];\n\n $fields = $this->getCompanyFields();\n $allCompanies = [];\n\n $fetchStart = microtime(true);\n foreach (array_chunk($crmIds, self::BATCH_SIZE) as $chunk) {\n try {\n $companies = $this->client->getCompaniesByIds($chunk, $fields);\n foreach ($companies as $companyData) {\n $allCompanies[] = $companyData;\n }\n } catch (\\Throwable $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Batch company fetch failed', [\n 'chunk_size' => \\count($chunk),\n 'error' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n }\n $fetchMs = (int) round((microtime(true) - $fetchStart) * 1000);\n\n $fetchedIds = array_map('strval', array_column($allCompanies, 'id'));\n $notFoundIds = array_values(array_diff(array_map('strval', $crmIds), $fetchedIds));\n\n if (! empty($notFoundIds)) {\n $this->logger->info('[' . $this->getDisplayName() . '] Company CRM IDs not found in HubSpot', [\n 'teamId' => $this->team->getId(),\n 'notFoundCount' => \\count($notFoundIds),\n 'notFoundIds' => $notFoundIds,\n 'requestedCount' => \\count($crmIds),\n 'fetchedCount' => \\count($allCompanies),\n ]);\n }\n\n $loopStart = microtime(true);\n foreach ($allCompanies as $companyData) {\n $accountStart = microtime(true);\n\n try {\n $account = $this->importAccount($companyData);\n if ($account !== null) {\n $result['success_count']++;\n }\n } catch (\\Throwable $e) {\n $crmId = $companyData['id'] ?? $companyData['properties']['hs_object_id'] ?? 'unknown';\n $result['failed_ids'][] = $crmId;\n $result['errors'][$crmId] = $e->getMessage();\n\n $this->logger->warning('[' . $this->getDisplayName() . '] Failed to import account', [\n 'crmId' => $crmId,\n 'error' => $e->getMessage(),\n ]);\n }\n\n $accountMs = (int) round((microtime(true) - $accountStart) * 1000);\n if ($accountMs > 1000) {\n $crmId = $companyData['id'] ?? $companyData['properties']['hs_object_id'] ?? 'unknown';\n $slowAccounts[] = ['crmId' => $crmId, 'ms' => $accountMs];\n }\n }\n $loopMs = (int) round((microtime(true) - $loopStart) * 1000);\n $totalMs = (int) round((microtime(true) - $batchStart) * 1000);\n\n $this->logger->info('[' . $this->getDisplayName() . '] importAccountBatch timing', [\n 'teamId' => $this->team->getId(),\n 'account_count' => \\count($allCompanies),\n 'requested_count' => \\count($crmIds),\n 'not_found_count' => \\count($notFoundIds),\n 'total_ms' => $totalMs,\n 'fetch_api_ms' => $fetchMs,\n 'accounts_loop_ms' => $loopMs,\n 'avg_account_ms' => ! empty($allCompanies) ? (int) round($loopMs / \\count($allCompanies)) : 0,\n 'slow_accounts_count' => \\count($slowAccounts),\n 'slow_accounts' => array_slice($slowAccounts, 0, 10),\n ]);\n\n return $result;\n }\n\n private function getCompanyFields(): array\n {\n return [\n 'country',\n 'name',\n 'phone',\n 'domain',\n 'hs_avatar_filemanager_key',\n 'industry',\n 'hubspot_owner_id',\n 'hs_object_id',\n 'createdate',\n 'hs_lastmodifieddate',\n ];\n }\n\n private function importAccount($crmData): ?Account\n {\n $crmProviderId = $crmData['id'] ?? $crmData['properties']['hs_object_id'] ?? null;\n\n $this->logger->info('[HubSpot] importAccount', [\n 'crm_provider_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n if (empty($crmData['properties'])) {\n $this->logger->info('[HubSpot] importAccount failed: empty properties', [\n 'crm_provider_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n return null;\n }\n\n $properties = $crmData['properties'];\n $crmId = (string) $properties['hs_object_id'];\n\n $countryCode = isset($properties['country']) ? $this->convertCountryNameToCode($properties['country']) : null;\n\n if (isset($properties['phone'])) {\n // Trim to our width and attempt to parse it.\n $number = mb_strimwidth($properties['phone'], 0, 25);\n $parsedNumber = parsePhoneNumber($countryCode, $number);\n } else {\n $parsedNumber = [];\n }\n\n $name = '[unknown]';\n if (isset($properties['name'])) {\n $name = $properties['name'];\n }\n\n $photoPath = $this->prospectPhotoPathService->getOrGeneratePhotoPath(\n $this->config,\n $crmId,\n Account::class,\n $crmId,\n $name\n );\n\n $industry = null;\n if (isset($properties['industry'])) {\n $industry = mb_strimwidth($properties['industry'], 0, 40);\n }\n\n $ownerId = $profile = null;\n if (isset($properties['hubspot_owner_id'])) {\n $ownerId = $properties['hubspot_owner_id'];\n $profile = $this->crmEntityRepository->findProfileByExternalId($this->config, (string) $ownerId);\n }\n\n $domain = null;\n if (isset($properties['domain'])) {\n $domain = StringUtil::resolveDomain($properties['domain']);\n }\n\n $remotelyCreatedAt = null;\n if (isset($properties['createdate']) && ! empty($properties['createdate'])) {\n $remotelyCreatedAt = Carbon::parse($properties['createdate']);\n }\n\n $data = [\n 'crm_provider_id' => $crmId,\n 'team_id' => $this->team->id,\n 'user_id' => $profile ? $profile->user_id : null,\n 'owner_id' => $ownerId,\n 'name' => mb_strimwidth($name, 0, 191),\n 'photo_path' => $photoPath,\n 'industry' => $industry,\n 'domain' => $domain !== null\n ? substr($domain, 0, 191)\n : null,\n 'phone' => $parsedNumber['phone'] ?? null,\n 'ext' => $parsedNumber['ext'] ?? null,\n 'country_code' => $countryCode,\n 'remotely_created_at' => $remotelyCreatedAt,\n ];\n\n return $this->crmEntityRepository->importAccount($this->config, $data);\n }\n\n public function deleteContact(string $crmProviderId): bool\n {\n try {\n $contact = $this->crmEntityRepository->findContactByExternalId($this->config, $crmProviderId);\n\n if (! $contact) {\n $this->logger->info('[HubSpot] Contact not found for deletion', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n // return success because we do not know which instance is the target\n return true;\n }\n\n $id = $contact->getId();\n\n $this->logger->info('[HubSpot] Deleting contact via webhook', [\n 'contact_id' => $id,\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n $contact->delete();\n DeleteContactJob::dispatch($id)->afterCommit();\n\n return true;\n } catch (Exception $e) {\n $this->logger->error('[HubSpot] Failed to delete contact via webhook', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n return false;\n }\n }\n\n public function deleteAccount(string $crmProviderId): bool\n {\n try {\n $account = $this->crmEntityRepository->findAccountByExternalId($this->config, $crmProviderId);\n\n if (! $account) {\n $this->logger->info('[HubSpot] Account not found for deletion', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n // return success because we do not know which instance is the target\n return true;\n }\n\n $id = $account->getId();\n\n $this->logger->info('[HubSpot] Deleting account via webhook', [\n 'account_id' => $id,\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n $account->delete();\n DeleteAccountJob::dispatch($id)->afterCommit();\n\n return true;\n } catch (Exception $e) {\n $this->logger->error('[HubSpot] Failed to delete account via webhook', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n return false;\n }\n }\n\n public function deleteOpportunity(string $crmProviderId): bool\n {\n try {\n $opportunity = $this->crmEntityRepository->findOpportunityByExternalId($this->config, $crmProviderId);\n\n if (! $opportunity) {\n $this->logger->info('[HubSpot] Opportunity not found for deletion', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n // return success because we do not know which instance is the target\n return true;\n }\n\n $id = $opportunity->getId();\n\n $this->logger->info('[HubSpot] Deleting opportunity via webhook', [\n 'opportunity_id' => $id,\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n $opportunity->delete();\n DeleteOpportunityJob::dispatch($id)->afterCommit();\n\n return true;\n } catch (Exception $e) {\n $this->logger->error('[HubSpot] Failed to delete opportunity via webhook', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n return false;\n }\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits;\n\nuse Carbon\\Carbon;\nuse Exception;\nuse Illuminate\\Support\\Str;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Jobs\\Crm\\Delete\\DeleteAccountJob;\nuse Jiminny\\Jobs\\Crm\\Delete\\DeleteContactJob;\nuse Jiminny\\Jobs\\Crm\\Delete\\DeleteOpportunityJob;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Repositories\\Crm\\CrmEntityRepository;\nuse Jiminny\\Services\\Avatar\\ProspectPhotoPathService;\nuse Jiminny\\Services\\Crm\\Hubspot\\HubspotClientInterface;\nuse Jiminny\\Services\\Crm\\Hubspot\\WebhookSyncBatchProcessor;\nuse Jiminny\\Utils\\StringUtil;\n\ntrait SyncCrmEntitiesTrait\n{\n use OpportunitySyncTrait;\n private const string CDN_URL = 'https://cdn2.hubspot.net/';\n protected CrmEntityRepository $crmEntityRepository;\n protected ProspectPhotoPathService $prospectPhotoPathService;\n\n private function getAssociationDataForCollection(array $collection, string $fromObject, string $toObject): array\n {\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $hsOpportunityIds = array_column($collection, 'id');\n\n return $this->client->getAssociationsData($hsOpportunityIds, $fromObject, $toObject);\n }\n\n private function importAssociationData(array $collection, array $associatedData): array\n {\n $data = [];\n if (! empty($associatedData[$collection['id']])) {\n foreach ($associatedData[$collection['id']] as $id) {\n $data[] = [\n 'id' => $id,\n ];\n }\n }\n\n return ['results' => $data];\n }\n\n /**\n * Sync contacts modified since a given date (manual sync mode).\n *\n * This method fetches contacts from HubSpot API based on modification date and\n * imports them one by one. It is used for:\n * - Manual sync commands (e.g., crm:sync-contact with --from parameter)\n * - Initial sync for new teams\n * - Backfill operations\n *\n * For regular sync webhook batchSyncContacts is used:\n *\n * @param Carbon $since Fetch contacts modified after this date\n * @param Carbon|null $to Optional end date for modification range\n *\n * @return int Number of contacts successfully synced\n */\n public function syncContacts(Carbon $since, ?Carbon $to = null): int\n {\n $syncCount = 0;\n\n try {\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $fields = $this->getContactFields();\n $payload = $this->payloadBuilder->getRecentlyUpdatedSearchPayload($since, $to, $fields);\n\n foreach ($this->client->getPaginatedDataGenerator($payload, 'contacts') as $hsContact) {\n $this->importContact($hsContact);\n $syncCount++;\n }\n } catch (Exception $exception) {\n $this->logger->error('[' . $this->getDisplayName() . '] Sync contacts failed', [\n 'teamId' => $this->team->getUuid(),\n 'reason' => $exception->getMessage(),\n ]);\n }\n\n return $syncCount;\n }\n\n /**\n * @inheritdoc\n */\n public function syncContact(string $crmId): ?Contact\n {\n try {\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $fields = $this->getContactFields();\n $hsContact = $this->client->getContactById($crmId, $fields);\n } catch (\\HubSpot\\Client\\Crm\\Contacts\\ApiException $e) {\n $this->logger->info('[' . $this->getDisplayName() . '] Contact fetch failed', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n return null;\n } catch (CrmException $e) {\n $this->logger->info('[' . $this->getDisplayName() . '] Contact not found', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n return null;\n }\n\n if (empty($hsContact['properties']) || empty($hsContact['id'])) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Contact data incomplete', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'has_properties' => ! empty($hsContact['properties']),\n 'has_id' => ! empty($hsContact['id']),\n ]);\n\n return null;\n }\n\n return $this->importContact($hsContact);\n }\n\n private function getContactFields(): array\n {\n return [\n 'associatedcompanyid',\n 'country',\n 'firstname',\n 'lastname',\n 'phone',\n 'mobilephone',\n 'email',\n 'photo',\n 'hs_avatar_filemanager_key',\n 'jobtitle',\n 'hubspot_owner_id',\n 'hs_object_id',\n 'createdate',\n 'hs_lastmodifieddate',\n ];\n }\n\n /**\n * @inheritdoc\n */\n private function importContact($crmData, array $accountMappings = []): ?Contact\n {\n $crmProviderId = $crmData['id'] ?? null;\n\n $this->logger->info('[HubSpot] importContact', [\n 'crm_provider_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n if (empty($crmData['properties'])) {\n $this->logger->info('[HubSpot] importContact failed: empty properties', [\n 'crm_provider_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n return null;\n }\n\n $properties = $crmData['properties'];\n $crmId = (string) $crmData['id'];\n\n $accountId = $this->resolveContactAccount($properties, $accountMappings);\n $data = $this->buildContactData($crmId, $properties, $accountId);\n\n return $this->crmEntityRepository->importContact($this->config, $data);\n }\n\n private function resolveContactAccount(array $properties, array $accountMappings): ?int\n {\n if (empty($properties['associatedcompanyid'])) {\n return null;\n }\n\n $companyId = (string) $properties['associatedcompanyid'];\n\n if (! empty($accountMappings)) {\n return $accountMappings[$companyId] ?? null;\n }\n\n return $this->crmEntityRepository->findAccountByExternalId(\n $this->team->getCrmConfiguration(),\n $companyId\n )?->getId() ?? $this->syncAccount($companyId)?->getId();\n }\n\n private function buildContactData(string $crmId, array $properties, ?int $accountId): array\n {\n $countryCode = $this->buildContactCountry($properties);\n $name = $this->buildContactName($properties);\n $photoPath = $this->teamService->generateAvatar(\n $crmId,\n empty($name) ? ($properties['email'] ?? 'N/A') : $name,\n );\n $parsedNumber = $this->buildContactPhone($countryCode, $properties);\n $mobileNumber = $this->buildContactMobilePhone($countryCode, $properties);\n\n $ownerId = $properties['hubspot_owner_id'] ?? null;\n $profile = $ownerId !== null\n ? $this->crmEntityRepository->findProfileByExternalId($this->config, (string) $ownerId)\n : null;\n\n $ext = (isset($parsedNumber['ext']) && is_string($parsedNumber['ext']) && strlen($parsedNumber['ext']) <= 10)\n ? $parsedNumber['ext']\n : null;\n\n $title = isset($properties['jobtitle']) ? mb_strimwidth($properties['jobtitle'], 0, 128) : null;\n $email = isset($properties['email']) ? mb_strimwidth($properties['email'], 0, 191) : null;\n $remotelyCreatedAt = ! empty($properties['createdate']) ? Carbon::parse($properties['createdate']) : null;\n\n return [\n 'crm_provider_id' => $crmId,\n 'team_id' => $this->team->getId(),\n 'account_id' => $accountId,\n 'user_id' => $profile?->getUserId(),\n 'owner_id' => $ownerId,\n 'name' => $name,\n 'title' => $title,\n 'email' => $email,\n 'country_code' => $countryCode,\n 'phone' => $parsedNumber['phone'] ?? null,\n 'mobile_phone' => $mobileNumber ?? null,\n 'ext' => $ext,\n 'photo_path' => $photoPath,\n 'remotely_created_at' => $remotelyCreatedAt,\n ];\n }\n\n /**\n * @param $properties\n */\n private function buildContactName($properties): string\n {\n if (is_array($properties)) {\n return $this->buildContactNameFromArray($properties);\n }\n\n return $this->buildContactNameFromObject($properties);\n }\n\n private function buildContactNameFromArray(array $properties): string\n {\n if (! empty($properties['name'])) {\n return mb_strimwidth($properties['name'], 0, 100);\n }\n\n $name = '';\n if (! empty($properties['firstname'])) {\n $name = $properties['firstname'] . ' ';\n }\n\n if (! empty($properties['lastname'])) {\n $name .= $properties['lastname'];\n }\n\n if ($name === '' && ! empty($properties['email'])) {\n $name = $properties['email'];\n }\n\n return mb_strimwidth($name, 0, 100);\n }\n\n private function buildContactNameFromObject($properties): string\n {\n $name = '';\n if (isset($properties->firstname)) {\n $name = $properties->firstname->value . ' ';\n }\n if (isset($properties->lastname)) {\n $name .= $properties->lastname->value;\n }\n if ($name === '' && isset($properties->email)) {\n $name = $properties->email->value;\n }\n\n return mb_strimwidth($name, 0, 100);\n }\n\n /**\n * @param $properties\n */\n private function buildContactPhone(?string $countryCode, $properties): ?array\n {\n if (is_array($properties) && empty($properties['phone']) === false) {\n $number = mb_strimwidth($properties['phone'], 0, 25);\n\n return parsePhoneNumber($countryCode, $number);\n } elseif (isset($properties->phone)) {\n $number = mb_strimwidth($properties->phone->value, 0, 25);\n\n return parsePhoneNumber($countryCode, $number);\n }\n\n return [];\n }\n\n /**\n * @param $properties\n */\n private function buildContactMobilePhone(?string $countryCode, $properties): ?string\n {\n return isset($properties['mobilephone'])\n ? Str::limit(phone_e164($countryCode, $properties['mobilephone']), 25, '')\n : null;\n }\n\n /**\n * @param $properties\n * @param $account\n */\n private function buildContactCountry($properties): ?string\n {\n if (is_array($properties) && empty($properties['country']) === false) {\n return $this->convertCountryNameToCode($properties['country']);\n }\n\n if (isset($properties->country)) {\n return $this->convertCountryNameToCode($properties->country->value);\n }\n\n return null;\n }\n\n /**\n * HubSpot doesn't have leads, so this method does nothing.\n *\n * @param Carbon $since\n * @param Carbon|null $to\n * @param string|null $crmProfileId\n *\n * @return int\n */\n public function syncLeads(Carbon $since, ?Carbon $to = null, ?string $crmProfileId = null): int\n {\n // Mark unused parameters to avoid code smell warnings\n unset($since, $to, $crmProfileId);\n\n return 0;\n }\n\n /**\n * HubSpot doesn't have leads.\n *\n * @param string $crmId\n *\n * @inheritdoc\n */\n public function syncLead(string $crmId): ?Lead\n {\n // Mark unused parameter to avoid code smell warnings\n unset($crmId);\n\n return null;\n }\n\n /**\n * Sync accounts (companies) modified since a given date (manual sync mode).\n *\n * This method fetches companies from HubSpot API based on modification date and\n * imports them one by one. It is used for:\n * - Manual sync commands (e.g., crm:sync-account with --from parameter)\n * - Initial sync for new teams\n * - Backfill operations\n *\n * For regular sync webhook batchSyncCompanies is used:\n *\n * @param Carbon $since Fetch companies modified after this date\n * @param Carbon|null $to Optional end date for modification range\n *\n * @return int Number of companies successfully synced\n */\n public function syncAccounts(Carbon $since, ?Carbon $to = null): int\n {\n $syncCount = 0;\n\n try {\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $fields = $this->getCompanyFields();\n $payload = $this->payloadBuilder->getRecentlyUpdatedSearchPayload($since, $to, $fields);\n\n foreach ($this->client->getPaginatedDataGenerator($payload, 'companies') as $hsAccount) {\n $this->importAccount($hsAccount);\n $syncCount++;\n }\n } catch (Exception $exception) {\n $this->logger->error('[' . $this->getDisplayName() . '] Sync accounts failed', [\n 'teamId' => $this->team->getUuid(),\n 'reason' => $exception->getMessage(),\n ]);\n }\n\n return $syncCount;\n }\n\n /**\n * @inheritdoc\n */\n public function syncAccount(string $crmId): ?Account\n {\n try {\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $fields = $this->getCompanyFields();\n $hsAccount = $this->client->getAccountById($crmId, $fields);\n } catch (\\HubSpot\\Client\\Crm\\Companies\\ApiException $e) {\n $this->logger->info('[' . $this->getDisplayName() . '] Account fetch failed', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n return null;\n } catch (CrmException $e) {\n $this->logger->info('[' . $this->getDisplayName() . '] Account not found', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n return null;\n }\n\n return $this->importAccount($hsAccount);\n }\n\n /**\n * Process webhook-collected contact batches.\n *\n * Drains Redis sets containing contact CRM IDs collected from webhook events\n * and dispatches ImportContactBatch jobs for batch processing.\n *\n * @return int Number of contact IDs dispatched to jobs\n */\n public function batchSyncContacts(): int\n {\n $configId = $this->team->getCrmConfiguration()->getId();\n\n return $this->batchProcessor->processBatchesForObjectType(\n WebhookSyncBatchProcessor::OBJECT_TYPE_CONTACT,\n $configId\n );\n }\n\n public function importContactBatchByIds(array $crmIds): array\n {\n $result = [\n 'success_count' => 0,\n 'failed_ids' => [],\n 'errors' => [],\n ];\n\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $batchStart = microtime(true);\n $slowContacts = [];\n\n $fetchStart = microtime(true);\n $allContacts = $this->fetchContactsByIdsInChunks($crmIds);\n $fetchMs = (int) round((microtime(true) - $fetchStart) * 1000);\n\n $fetchedIds = array_map('strval', array_column($allContacts, 'id'));\n $notFoundIds = array_values(array_diff(array_map('strval', $crmIds), $fetchedIds));\n\n if (! empty($notFoundIds)) {\n $this->logger->info('[' . $this->getDisplayName() . '] Contact CRM IDs not found in HubSpot', [\n 'teamId' => $this->team->getId(),\n 'notFoundCount' => \\count($notFoundIds),\n 'notFoundIds' => $notFoundIds,\n 'requestedCount' => \\count($crmIds),\n 'fetchedCount' => \\count($allContacts),\n ]);\n }\n\n if (empty($allContacts)) {\n return $result;\n }\n\n $prepareStart = microtime(true);\n $accountMappings = $this->prepareAccountMappingsForContacts($allContacts);\n $prepareAccountsMs = (int) round((microtime(true) - $prepareStart) * 1000);\n\n $loopStart = microtime(true);\n foreach ($allContacts as $contactData) {\n $contactStart = microtime(true);\n\n try {\n $contact = $this->importContact($contactData, $accountMappings);\n if ($contact !== null) {\n $result['success_count']++;\n }\n } catch (\\Throwable $e) {\n $crmId = $contactData['id'] ?? 'unknown';\n $result['failed_ids'][] = $crmId;\n $result['errors'][$crmId] = $e->getMessage();\n\n $this->logger->warning('[' . $this->getDisplayName() . '] Failed to import contact', [\n 'teamId' => $this->team->getId(),\n 'crmId' => $crmId,\n 'error' => $e->getMessage(),\n ]);\n }\n\n $contactMs = (int) round((microtime(true) - $contactStart) * 1000);\n if ($contactMs > 1000) {\n $slowContacts[] = ['crmId' => $contactData['id'] ?? 'unknown', 'ms' => $contactMs];\n }\n }\n $loopMs = (int) round((microtime(true) - $loopStart) * 1000);\n $totalMs = (int) round((microtime(true) - $batchStart) * 1000);\n\n $this->logger->info('[' . $this->getDisplayName() . '] importContactBatch timing', [\n 'teamId' => $this->team->getId(),\n 'contact_count' => \\count($allContacts),\n 'requested_count' => \\count($crmIds),\n 'not_found_count' => \\count($notFoundIds),\n 'total_ms' => $totalMs,\n 'fetch_api_ms' => $fetchMs,\n 'prepare_accounts_ms' => $prepareAccountsMs,\n 'contacts_loop_ms' => $loopMs,\n 'avg_contact_ms' => ! empty($allContacts) ? (int) round($loopMs / \\count($allContacts)) : 0,\n 'slow_contacts_count' => \\count($slowContacts),\n 'slow_contacts' => array_slice($slowContacts, 0, 10),\n ]);\n\n return $result;\n }\n\n private function fetchContactsByIdsInChunks(array $crmIds): array\n {\n $fields = $this->getContactFields();\n $allContacts = [];\n\n foreach (array_chunk($crmIds, self::BATCH_SIZE) as $chunk) {\n try {\n $contacts = $this->client->getContactsByIds($chunk, $fields);\n foreach ($contacts as $contactData) {\n $allContacts[] = $contactData;\n }\n } catch (\\Throwable $e) {\n // @TODO what will happen if this exception is thrown\n $this->logger->warning('[' . $this->getDisplayName() . '] Batch contact fetch failed', [\n 'chunk_size' => \\count($chunk),\n 'error' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n }\n\n return $allContacts;\n }\n\n private function prepareAccountMappingsForContacts(array $contacts): array\n {\n $companyIds = [];\n foreach ($contacts as $contact) {\n $companyId = $contact['properties']['associatedcompanyid'] ?? null;\n if ($companyId !== null && $companyId !== '') {\n $companyIds[] = (string) $companyId;\n }\n }\n\n $companyIds = array_unique($companyIds);\n\n if (empty($companyIds)) {\n return [];\n }\n\n $mappings = $this->crmEntityRepository\n ->getExistingAccountIdsMap($this->config, $companyIds);\n\n $missingCompanyIds = array_diff($companyIds, array_keys($mappings));\n\n if (empty($missingCompanyIds)) {\n return $mappings;\n }\n\n $this->logger->info('[' . $this->getDisplayName() . '] Batch syncing missing accounts for contacts', [\n 'teamId' => $this->team->getId(),\n 'total_companies' => \\count($companyIds),\n 'existing_companies' => \\count($mappings),\n 'missing_companies' => \\count($missingCompanyIds),\n ]);\n\n try {\n $syncedAccounts = $this->batchSyncAccountsForContacts($missingCompanyIds);\n $mappings = array_merge($mappings, $syncedAccounts);\n } catch (\\Throwable $e) {\n $this->logger->error('[' . $this->getDisplayName() . '] Failed to batch sync missing accounts', [\n 'teamId' => $this->team->getId(),\n 'missingCompanyIds' => $missingCompanyIds,\n 'missingCount' => count($missingCompanyIds),\n 'error' => $e->getMessage(),\n ]);\n }\n\n return $mappings;\n }\n\n private function batchSyncAccountsForContacts(array $companyIds): array\n {\n $syncedAccounts = [];\n $fields = $this->getCompanyFields();\n\n foreach (array_chunk($companyIds, self::BATCH_SIZE) as $chunk) {\n try {\n $companies = $this->client->getCompaniesByIds($chunk, $fields);\n\n foreach ($companies as $companyData) {\n try {\n $account = $this->importAccount($companyData);\n if ($account) {\n $syncedAccounts[$account->getCrmProviderId()] = $account->getId();\n }\n } catch (\\Throwable $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Failed to import account in batch', [\n 'teamId' => $this->team->getId(),\n 'companyId' => $companyData['id'] ?? 'unknown',\n 'error' => $e->getMessage(),\n ]);\n }\n }\n } catch (\\Throwable $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Batch company fetch failed', [\n 'teamId' => $this->team->getId(),\n 'chunk_size' => \\count($chunk),\n 'error' => $e->getMessage(),\n ]);\n }\n }\n\n return $syncedAccounts;\n }\n\n /**\n * Process webhook-collected company batches.\n *\n * Drains Redis sets containing company CRM IDs collected from webhook events\n * and dispatches ImportAccountBatch jobs for batch processing.\n *\n * @return int Number of company IDs dispatched to jobs\n */\n public function batchSyncCompanies(): int\n {\n $configId = $this->team->getCrmConfiguration()->getId();\n\n return $this->batchProcessor->processBatchesForObjectType(\n WebhookSyncBatchProcessor::OBJECT_TYPE_COMPANY,\n $configId\n );\n }\n\n public function importAccountBatchByIds(array $crmIds): array\n {\n $result = [\n 'success_count' => 0,\n 'failed_ids' => [],\n 'errors' => [],\n ];\n\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $batchStart = microtime(true);\n $slowAccounts = [];\n\n $fields = $this->getCompanyFields();\n $allCompanies = [];\n\n $fetchStart = microtime(true);\n foreach (array_chunk($crmIds, self::BATCH_SIZE) as $chunk) {\n try {\n $companies = $this->client->getCompaniesByIds($chunk, $fields);\n foreach ($companies as $companyData) {\n $allCompanies[] = $companyData;\n }\n } catch (\\Throwable $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Batch company fetch failed', [\n 'chunk_size' => \\count($chunk),\n 'error' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n }\n $fetchMs = (int) round((microtime(true) - $fetchStart) * 1000);\n\n $fetchedIds = array_map('strval', array_column($allCompanies, 'id'));\n $notFoundIds = array_values(array_diff(array_map('strval', $crmIds), $fetchedIds));\n\n if (! empty($notFoundIds)) {\n $this->logger->info('[' . $this->getDisplayName() . '] Company CRM IDs not found in HubSpot', [\n 'teamId' => $this->team->getId(),\n 'notFoundCount' => \\count($notFoundIds),\n 'notFoundIds' => $notFoundIds,\n 'requestedCount' => \\count($crmIds),\n 'fetchedCount' => \\count($allCompanies),\n ]);\n }\n\n $loopStart = microtime(true);\n foreach ($allCompanies as $companyData) {\n $accountStart = microtime(true);\n\n try {\n $account = $this->importAccount($companyData);\n if ($account !== null) {\n $result['success_count']++;\n }\n } catch (\\Throwable $e) {\n $crmId = $companyData['id'] ?? $companyData['properties']['hs_object_id'] ?? 'unknown';\n $result['failed_ids'][] = $crmId;\n $result['errors'][$crmId] = $e->getMessage();\n\n $this->logger->warning('[' . $this->getDisplayName() . '] Failed to import account', [\n 'crmId' => $crmId,\n 'error' => $e->getMessage(),\n ]);\n }\n\n $accountMs = (int) round((microtime(true) - $accountStart) * 1000);\n if ($accountMs > 1000) {\n $crmId = $companyData['id'] ?? $companyData['properties']['hs_object_id'] ?? 'unknown';\n $slowAccounts[] = ['crmId' => $crmId, 'ms' => $accountMs];\n }\n }\n $loopMs = (int) round((microtime(true) - $loopStart) * 1000);\n $totalMs = (int) round((microtime(true) - $batchStart) * 1000);\n\n $this->logger->info('[' . $this->getDisplayName() . '] importAccountBatch timing', [\n 'teamId' => $this->team->getId(),\n 'account_count' => \\count($allCompanies),\n 'requested_count' => \\count($crmIds),\n 'not_found_count' => \\count($notFoundIds),\n 'total_ms' => $totalMs,\n 'fetch_api_ms' => $fetchMs,\n 'accounts_loop_ms' => $loopMs,\n 'avg_account_ms' => ! empty($allCompanies) ? (int) round($loopMs / \\count($allCompanies)) : 0,\n 'slow_accounts_count' => \\count($slowAccounts),\n 'slow_accounts' => array_slice($slowAccounts, 0, 10),\n ]);\n\n return $result;\n }\n\n private function getCompanyFields(): array\n {\n return [\n 'country',\n 'name',\n 'phone',\n 'domain',\n 'hs_avatar_filemanager_key',\n 'industry',\n 'hubspot_owner_id',\n 'hs_object_id',\n 'createdate',\n 'hs_lastmodifieddate',\n ];\n }\n\n private function importAccount($crmData): ?Account\n {\n $crmProviderId = $crmData['id'] ?? $crmData['properties']['hs_object_id'] ?? null;\n\n $this->logger->info('[HubSpot] importAccount', [\n 'crm_provider_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n if (empty($crmData['properties'])) {\n $this->logger->info('[HubSpot] importAccount failed: empty properties', [\n 'crm_provider_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n return null;\n }\n\n $properties = $crmData['properties'];\n $crmId = (string) $properties['hs_object_id'];\n\n $countryCode = isset($properties['country']) ? $this->convertCountryNameToCode($properties['country']) : null;\n\n if (isset($properties['phone'])) {\n // Trim to our width and attempt to parse it.\n $number = mb_strimwidth($properties['phone'], 0, 25);\n $parsedNumber = parsePhoneNumber($countryCode, $number);\n } else {\n $parsedNumber = [];\n }\n\n $name = '[unknown]';\n if (isset($properties['name'])) {\n $name = $properties['name'];\n }\n\n $photoPath = $this->prospectPhotoPathService->getOrGeneratePhotoPath(\n $this->config,\n $crmId,\n Account::class,\n $crmId,\n $name\n );\n\n $industry = null;\n if (isset($properties['industry'])) {\n $industry = mb_strimwidth($properties['industry'], 0, 40);\n }\n\n $ownerId = $profile = null;\n if (isset($properties['hubspot_owner_id'])) {\n $ownerId = $properties['hubspot_owner_id'];\n $profile = $this->crmEntityRepository->findProfileByExternalId($this->config, (string) $ownerId);\n }\n\n $domain = null;\n if (isset($properties['domain'])) {\n $domain = StringUtil::resolveDomain($properties['domain']);\n }\n\n $remotelyCreatedAt = null;\n if (isset($properties['createdate']) && ! empty($properties['createdate'])) {\n $remotelyCreatedAt = Carbon::parse($properties['createdate']);\n }\n\n $data = [\n 'crm_provider_id' => $crmId,\n 'team_id' => $this->team->id,\n 'user_id' => $profile ? $profile->user_id : null,\n 'owner_id' => $ownerId,\n 'name' => mb_strimwidth($name, 0, 191),\n 'photo_path' => $photoPath,\n 'industry' => $industry,\n 'domain' => $domain !== null\n ? substr($domain, 0, 191)\n : null,\n 'phone' => $parsedNumber['phone'] ?? null,\n 'ext' => $parsedNumber['ext'] ?? null,\n 'country_code' => $countryCode,\n 'remotely_created_at' => $remotelyCreatedAt,\n ];\n\n return $this->crmEntityRepository->importAccount($this->config, $data);\n }\n\n public function deleteContact(string $crmProviderId): bool\n {\n try {\n $contact = $this->crmEntityRepository->findContactByExternalId($this->config, $crmProviderId);\n\n if (! $contact) {\n $this->logger->info('[HubSpot] Contact not found for deletion', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n // return success because we do not know which instance is the target\n return true;\n }\n\n $id = $contact->getId();\n\n $this->logger->info('[HubSpot] Deleting contact via webhook', [\n 'contact_id' => $id,\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n $contact->delete();\n DeleteContactJob::dispatch($id)->afterCommit();\n\n return true;\n } catch (Exception $e) {\n $this->logger->error('[HubSpot] Failed to delete contact via webhook', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n return false;\n }\n }\n\n public function deleteAccount(string $crmProviderId): bool\n {\n try {\n $account = $this->crmEntityRepository->findAccountByExternalId($this->config, $crmProviderId);\n\n if (! $account) {\n $this->logger->info('[HubSpot] Account not found for deletion', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n // return success because we do not know which instance is the target\n return true;\n }\n\n $id = $account->getId();\n\n $this->logger->info('[HubSpot] Deleting account via webhook', [\n 'account_id' => $id,\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n $account->delete();\n DeleteAccountJob::dispatch($id)->afterCommit();\n\n return true;\n } catch (Exception $e) {\n $this->logger->error('[HubSpot] Failed to delete account via webhook', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n return false;\n }\n }\n\n public function deleteOpportunity(string $crmProviderId): bool\n {\n try {\n $opportunity = $this->crmEntityRepository->findOpportunityByExternalId($this->config, $crmProviderId);\n\n if (! $opportunity) {\n $this->logger->info('[HubSpot] Opportunity not found for deletion', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n // return success because we do not know which instance is the target\n return true;\n }\n\n $id = $opportunity->getId();\n\n $this->logger->info('[HubSpot] Deleting opportunity via webhook', [\n 'opportunity_id' => $id,\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n $opportunity->delete();\n DeleteOpportunityJob::dispatch($id)->afterCommit();\n\n return true;\n } catch (Exception $e) {\n $this->logger->error('[HubSpot] Failed to delete opportunity via webhook', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n return false;\n }\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"app ~/jiminny/app, folder","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".circleci, folder","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".cursor, folder","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".github","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".sonarlint, folder","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".vscode, folder","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".windsurf, folder","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"app, sources root","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Actions, folder","depth":8,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Component, folder","depth":8,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Acl, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ActionItems, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Activity, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ActivityAnalytics, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ActivitySearch, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"AiActivityType, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"AiAutomation, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"AiCallScoring, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"AskAnything, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Dtos, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Events, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"AskAnythingPromptService.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"HistoryService.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"AskJiminnyAi, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"AWS, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"BillingManagement, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Cache, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"CoachingFeedback, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Country, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"CustomerApi, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Database, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Datadog, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"DateTime, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"DealInsights, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"DealRisks, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ElasticSearch","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Eloquent","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Encoding","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Encryption","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ES","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Faker","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"FeatureFlags","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"FFMpeg","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"FileSystem","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Gecko","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Gong","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"GuzzleHttp","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"KeyPoints","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Kiosk","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"LanguageDetection","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"LiveFeed","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Locks","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Math","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"MediaPipeline","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"MeetingBot, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"MobileSettings, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Model, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Notification, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Nudge, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ParagraphBreaker, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ParticipantSpeech, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"PartitionedCookie, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"PlaybackPage, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Playlist, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Prophet, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ProphetAi, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ProsperWorks, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Queue, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Job, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"RateLimitAware.php, abstract class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"RateLimitAwareWrapper.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"BotsQueueConstants.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Constants.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ProcessingQueueConstants.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Router, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Saml2, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"SCIM, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Seeder, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Sentry, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Serializer, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Settings, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Sidekick, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Slack, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"TeamInsights, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"TimeMemoryMapper, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Transcription, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"TranscriptionSummary, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Twilio, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Uploader, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"UrlGenerator, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Utility, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Exceptions, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Service, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"BaseRateLimiter.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"EfficientJsonParser.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ProviderRateLimiter.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"RateLimiterInstance.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Uuid, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Waveform, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Webhooks, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Workflow, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Configuration, folder","depth":8,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Console, folder","depth":8,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Commands, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Activities, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Analytics, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Calendars, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Crm, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Hubspot, folder","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"IntegrationApp, folder","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Traits, folder","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"AddLayoutEntities.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"AutologDelayedCommand.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"BullhornCommandAbstract.php, abstract class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"BullhornPingCommand.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"BullhornSearchCommand.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"BullhornSessionCommand.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"CheckActivityLoggableCommand.php, final class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"CleanDuplicateFieldDataCommand.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"FullSyncOpportunityCommand.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"LogActivitiesCommand.php, final class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ManageSyncStrategyCommand.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"MatchCrmObjectsCommand.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"MatchOpportunityActivitiesCommand.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"MigrateProvider.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ProcessHubspotObjectsSyncBatches.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"PurgeDeletedOpportunitiesCommand.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ResetGovernorLimits.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"SendNotLogged.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"SetupActivityTypeForFollowUp.php, final class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"SetupCloseCrm.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"SetupCopperCrm.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"SetupCrmCommand.php, abstract class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"SetupLayouts.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"SyncAccount.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"SyncContact.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"SyncFieldMetadata.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"SyncHubspotActiveDeals.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"SyncHubspotObjects.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"SyncLead.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"SyncObjects.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"SyncOpportunitiesMissingFieldDataCommand.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"SyncOpportunity.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"SyncProfileMetadata.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"SyncTeamMetadata.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"UpdateOpportunitySpecifications.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"DealInsights, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Dev, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"AddRateLimitCommand.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"FixHubSpotTokens.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"FixMissMatchedCrmActivitiesCommand.php, final class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ImportCallsCommand.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"MonitorSocialAccountsState.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Dialers, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"DTOs, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Elasticsearch, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"EngagementStats, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"GeckoExport, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Livestream, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Mailboxes, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Migrate, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"PlaybackThemes, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Playbooks, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Playlists, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Postmark, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ProphetAi, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Reports, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"AutomatedReportsCommand.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"AutomatedReportsRetentionPolicyCommand.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"AutomatedReportsSendCommand.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"CreateMockAskJiminnyReportResultCommand.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"DeleteReportCommand.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"GenerateMarketingReport.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Team.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Usage.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Slack, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Teams, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Tracks, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Transcription, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Twilio, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Users, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Vocabulary, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Zoom, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Command.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"CreateDatabaseUsers.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"DatabaseTableCount.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"DeleteOldAiCrmNotesCommand.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"DeleteS3LeftoversCommand.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"DevPostmanCommand.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"DiarizeViaAiParticipantIdentificationCommand.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"EncryptTokensCommand.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"EngagementStatsRegenerateCommand.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"FeatureFlagsHelper.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"FixCrossTenantIssues.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"FlushRolesPermissionsCache.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"GenerateInternalWebhookToken.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"GroupSetDefaultLanguageCommand.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"HelperTruncateCoachingTables.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"HubspotJournalPollingCommand.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"HubspotWebhookServiceCommand.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ImportRecording.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ImportUsersFromCsvFile.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"IterateUsersCommand.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"JiminnyCacheClearCommand.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"JiminnyDebugCommand.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"JiminnySetEncryptedTokenManagerModeCommand.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"JiminnyTokenInfoCommand.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"MakeSlackLiveCoachingChatNotesOn.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ManageScimForTeam.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"MarkBranchForEnvironmentPipelineCommand.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"MuteOrganizerChannel.php, class","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"PhpApm.php, class","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"PropagateCoachingFeedbackCreatedAtToSectionFeedbacks.php, class","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"PurgeConferences.php, class","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"PurgeSoftDeletedOpportunitiesCommand.php, class","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"PurgeSyncBatchesCommand.php, class","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"RecalculateDealRisksCommand.php, class","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"RemoveDeleteMarkersCommand.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"RemoveExpiredNudgesCommand.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"RemoveUnusedParticipantSpeechesCommand.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ResetElasticSearch.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"RestoreActivityCrmProviderIdCommand.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"RestoreActivityTypeCommand.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"RunAiCallScoringForUntypedActivitiesCommand.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"SeedActivities.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"SendNudgeExpirationWarningsCommand.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"SyncActivity.php, class","depth":10,"on_screen":false,"role_description":"text"}]...
|
-395738599667889143
|
5036074921934260454
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
Editor for custom.log
Sync Changes
Hide This Notification
Code changed:
Hide
Analyzing…
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\ServiceTraits;
use Carbon\Carbon;
use Exception;
use Illuminate\Support\Str;
use Jiminny\Exceptions\CrmException;
use Jiminny\Jobs\Crm\Delete\DeleteAccountJob;
use Jiminny\Jobs\Crm\Delete\DeleteContactJob;
use Jiminny\Jobs\Crm\Delete\DeleteOpportunityJob;
use Jiminny\Models\Account;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Repositories\Crm\CrmEntityRepository;
use Jiminny\Services\Avatar\ProspectPhotoPathService;
use Jiminny\Services\Crm\Hubspot\HubspotClientInterface;
use Jiminny\Services\Crm\Hubspot\WebhookSyncBatchProcessor;
use Jiminny\Utils\StringUtil;
trait SyncCrmEntitiesTrait
{
use OpportunitySyncTrait;
private const string CDN_URL = '[URL_WITH_CREDENTIALS] Carbon $since Fetch contacts modified after this date
* @param Carbon|null $to Optional end date for modification range
*
* @return int Number of contacts successfully synced
*/
public function syncContacts(Carbon $since, ?Carbon $to = null): int
{
$syncCount = 0;
try {
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$fields = $this->getContactFields();
$payload = $this->payloadBuilder->getRecentlyUpdatedSearchPayload($since, $to, $fields);
foreach ($this->client->getPaginatedDataGenerator($payload, 'contacts') as $hsContact) {
$this->importContact($hsContact);
$syncCount++;
}
} catch (Exception $exception) {
$this->logger->error('[' . $this->getDisplayName() . '] Sync contacts failed', [
'teamId' => $this->team->getUuid(),
'reason' => $exception->getMessage(),
]);
}
return $syncCount;
}
/**
* @inheritdoc
*/
public function syncContact(string $crmId): ?Contact
{
try {
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$fields = $this->getContactFields();
$hsContact = $this->client->getContactById($crmId, $fields);
} catch (\HubSpot\Client\Crm\Contacts\ApiException $e) {
$this->logger->info('[' . $this->getDisplayName() . '] Contact fetch failed', [
'teamId' => $this->team->getUuid(),
'crmId' => $crmId,
'reason' => $e->getMessage(),
]);
return null;
} catch (CrmException $e) {
$this->logger->info('[' . $this->getDisplayName() . '] Contact not found', [
'teamId' => $this->team->getUuid(),
'crmId' => $crmId,
'reason' => $e->getMessage(),
]);
return null;
}
if (empty($hsContact['properties']) || empty($hsContact['id'])) {
$this->logger->warning('[' . $this->getDisplayName() . '] Contact data incomplete', [
'teamId' => $this->team->getUuid(),
'crmId' => $crmId,
'has_properties' => ! empty($hsContact['properties']),
'has_id' => ! empty($hsContact['id']),
]);
return null;
}
return $this->importContact($hsContact);
}
private function getContactFields(): array
{
return [
'associatedcompanyid',
'country',
'firstname',
'lastname',
'phone',
'mobilephone',
'email',
'photo',
'hs_avatar_filemanager_key',
'jobtitle',
'hubspot_owner_id',
'hs_object_id',
'createdate',
'hs_lastmodifieddate',
];
}
/**
* @inheritdoc
*/
private function importContact($crmData, array $accountMappings = []): ?Contact
{
$crmProviderId = $crmData['id'] ?? null;
$this->logger->info('[HubSpot] importContact', [
'crm_provider_id' => $crmProviderId,
'config_id' => $this->config->getId(),
]);
if (empty($crmData['properties'])) {
$this->logger->info('[HubSpot] importContact failed: empty properties', [
'crm_provider_id' => $crmProviderId,
'config_id' => $this->config->getId(),
]);
return null;
}
$properties = $crmData['properties'];
$crmId = (string) $crmData['id'];
$accountId = $this->resolveContactAccount($properties, $accountMappings);
$data = $this->buildContactData($crmId, $properties, $accountId);
return $this->crmEntityRepository->importContact($this->config, $data);
}
private function resolveContactAccount(array $properties, array $accountMappings): ?int
{
if (empty($properties['associatedcompanyid'])) {
return null;
}
$companyId = (string) $properties['associatedcompanyid'];
if (! empty($accountMappings)) {
return $accountMappings[$companyId] ?? null;
}
return $this->crmEntityRepository->findAccountByExternalId(
$this->team->getCrmConfiguration(),
$companyId
)?->getId() ?? $this->syncAccount($companyId)?->getId();
}
private function buildContactData(string $crmId, array $properties, ?int $accountId): array
{
$countryCode = $this->buildContactCountry($properties);
$name = $this->buildContactName($properties);
$photoPath = $this->teamService->generateAvatar(
$crmId,
empty($name) ? ($properties['email'] ?? 'N/A') : $name,
);
$parsedNumber = $this->buildContactPhone($countryCode, $properties);
$mobileNumber = $this->buildContactMobilePhone($countryCode, $properties);
$ownerId = $properties['hubspot_owner_id'] ?? null;
$profile = $ownerId !== null
? $this->crmEntityRepository->findProfileByExternalId($this->config, (string) $ownerId)
: null;
$ext = (isset($parsedNumber['ext']) && is_string($parsedNumber['ext']) && strlen($parsedNumber['ext']) <= 10)
? $parsedNumber['ext']
: null;
$title = isset($properties['jobtitle']) ? mb_strimwidth($properties['jobtitle'], 0, 128) : null;
$email = isset($properties['email']) ? mb_strimwidth($properties['email'], 0, 191) : null;
$remotelyCreatedAt = ! empty($properties['createdate']) ? Carbon::parse($properties['createdate']) : null;
return [
'crm_provider_id' => $crmId,
'team_id' => $this->team->getId(),
'account_id' => $accountId,
'user_id' => $profile?->getUserId(),
'owner_id' => $ownerId,
'name' => $name,
'title' => $title,
'email' => $email,
'country_code' => $countryCode,
'phone' => $parsedNumber['phone'] ?? null,
'mobile_phone' => $mobileNumber ?? null,
'ext' => $ext,
'photo_path' => $photoPath,
'remotely_created_at' => $remotelyCreatedAt,
];
}
/**
* @param $properties
*/
private function buildContactName($properties): string
{
if (is_array($properties)) {
return $this->buildContactNameFromArray($properties);
}
return $this->buildContactNameFromObject($properties);
}
private function buildContactNameFromArray(array $properties): string
{
if (! empty($properties['name'])) {
return mb_strimwidth($properties['name'], 0, 100);
}
$name = '';
if (! empty($properties['firstname'])) {
$name = $properties['firstname'] . ' ';
}
if (! empty($properties['lastname'])) {
$name .= $properties['lastname'];
}
if ($name === '' && ! empty($properties['email'])) {
$name = $properties['email'];
}
return mb_strimwidth($name, 0, 100);
}
private function buildContactNameFromObject($properties): string
{
$name = '';
if (isset($properties->firstname)) {
$name = $properties->firstname->value . ' ';
}
if (isset($properties->lastname)) {
$name .= $properties->lastname->value;
}
if ($name === '' && isset($properties->email)) {
$name = $properties->email->value;
}
return mb_strimwidth($name, 0, 100);
}
/**
* @param $properties
*/
private function buildContactPhone(?string $countryCode, $properties): ?array
{
if (is_array($properties) && empty($properties['phone']) === false) {
$number = mb_strimwidth($properties['phone'], 0, 25);
return parsePhoneNumber($countryCode, $number);
} elseif (isset($properties->phone)) {
$number = mb_strimwidth($properties->phone->value, 0, 25);
return parsePhoneNumber($countryCode, $number);
}
return [];
}
/**
* @param $properties
*/
private function buildContactMobilePhone(?string $countryCode, $properties): ?string
{
return isset($properties['mobilephone'])
? Str::limit(phone_e164($countryCode, $properties['mobilephone']), 25, '')
: null;
}
/**
* @param $properties
* @param $account
*/
private function buildContactCountry($properties): ?string
{
if (is_array($properties) && empty($properties['country']) === false) {
return $this->convertCountryNameToCode($properties['country']);
}
if (isset($properties->country)) {
return $this->convertCountryNameToCode($properties->country->value);
}
return null;
}
/**
* HubSpot doesn't have leads, so this method does nothing.
*
* @param Carbon $since
* @param Carbon|null $to
* @param string|null $crmProfileId
*
* @return int
*/
public function syncLeads(Carbon $since, ?Carbon $to = null, ?string $crmProfileId = null): int
{
// Mark unused parameters to avoid code smell warnings
unset($since, $to, $crmProfileId);
return 0;
}
/**
* HubSpot doesn't have leads.
*
* @param string $crmId
*
* @inheritdoc
*/
public function syncLead(string $crmId): ?Lead
{
// Mark unused parameter to avoid code smell warnings
unset($crmId);
return null;
}
/**
* Sync accounts (companies) modified since a given date (manual sync mode).
*
* This method fetches companies from HubSpot API based on modification date and
* imports them one by one. It is used for:
* - Manual sync commands (e.g., crm:sync-account with --from parameter)
* - Initial sync for new teams
* - Backfill operations
*
* For regular sync webhook batchSyncCompanies is used:
*
* @param Carbon $since Fetch companies modified after this date
* @param Carbon|null $to Optional end date for modification range
*
* @return int Number of companies successfully synced
*/
public function syncAccounts(Carbon $since, ?Carbon $to = null): int
{
$syncCount = 0;
try {
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$fields = $this->getCompanyFields();
$payload = $this->payloadBuilder->getRecentlyUpdatedSearchPayload($since, $to, $fields);
foreach ($this->client->getPaginatedDataGenerator($payload, 'companies') as $hsAccount) {
$this->importAccount($hsAccount);
$syncCount++;
}
} catch (Exception $exception) {
$this->logger->error('[' . $this->getDisplayName() . '] Sync accounts failed', [
'teamId' => $this->team->getUuid(),
'reason' => $exception->getMessage(),
]);
}
return $syncCount;
}
/**
* @inheritdoc
*/
public function syncAccount(string $crmId): ?Account
{
try {
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$fields = $this->getCompanyFields();
$hsAccount = $this->client->getAccountById($crmId, $fields);
} catch (\HubSpot\Client\Crm\Companies\ApiException $e) {
$this->logger->info('[' . $this->getDisplayName() . '] Account fetch failed', [
'teamId' => $this->team->getUuid(),
'crmId' => $crmId,
'reason' => $e->getMessage(),
]);
return null;
} catch (CrmException $e) {
$this->logger->info('[' . $this->getDisplayName() . '] Account not found', [
'teamId' => $this->team->getUuid(),
'crmId' => $crmId,
'reason' => $e->getMessage(),
]);
return null;
}
return $this->importAccount($hsAccount);
}
/**
* Process webhook-collected contact batches.
*
* Drains Redis sets containing contact CRM IDs collected from webhook events
* and dispatches ImportContactBatch jobs for batch processing.
*
* @return int Number of contact IDs dispatched to jobs
*/
public function batchSyncContacts(): int
{
$configId = $this->team->getCrmConfiguration()->getId();
return $this->batchProcessor->processBatchesForObjectType(
WebhookSyncBatchProcessor::OBJECT_TYPE_CONTACT,
$configId
);
}
public function importContactBatchByIds(array $crmIds): array
{
$result = [
'success_count' => 0,
'failed_ids' => [],
'errors' => [],
];
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$batchStart = microtime(true);
$slowContacts = [];
$fetchStart = microtime(true);
$allContacts = $this->fetchContactsByIdsInChunks($crmIds);
$fetchMs = (int) round((microtime(true) - $fetchStart) * 1000);
$fetchedIds = array_map('strval', array_column($allContacts, 'id'));
$notFoundIds = array_values(array_diff(array_map('strval', $crmIds), $fetchedIds));
if (! empty($notFoundIds)) {
$this->logger->info('[' . $this->getDisplayName() . '] Contact CRM IDs not found in HubSpot', [
'teamId' => $this->team->getId(),
'notFoundCount' => \count($notFoundIds),
'notFoundIds' => $notFoundIds,
'requestedCount' => \count($crmIds),
'fetchedCount' => \count($allContacts),
]);
}
if (empty($allContacts)) {
return $result;
}
$prepareStart = microtime(true);
$accountMappings = $this->prepareAccountMappingsForContacts($allContacts);
$prepareAccountsMs = (int) round((microtime(true) - $prepareStart) * 1000);
$loopStart = microtime(true);
foreach ($allContacts as $contactData) {
$contactStart = microtime(true);
try {
$contact = $this->importContact($contactData, $accountMappings);
if ($contact !== null) {
$result['success_count']++;
}
} catch (\Throwable $e) {
$crmId = $contactData['id'] ?? 'unknown';
$result['failed_ids'][] = $crmId;
$result['errors'][$crmId] = $e->getMessage();
$this->logger->warning('[' . $this->getDisplayName() . '] Failed to import contact', [
'teamId' => $this->team->getId(),
'crmId' => $crmId,
'error' => $e->getMessage(),
]);
}
$contactMs = (int) round((microtime(true) - $contactStart) * 1000);
if ($contactMs > 1000) {
$slowContacts[] = ['crmId' => $contactData['id'] ?? 'unknown', 'ms' => $contactMs];
}
}
$loopMs = (int) round((microtime(true) - $loopStart) * 1000);
$totalMs = (int) round((microtime(true) - $batchStart) * 1000);
$this->logger->info('[' . $this->getDisplayName() . '] importContactBatch timing', [
'teamId' => $this->team->getId(),
'contact_count' => \count($allContacts),
'requested_count' => \count($crmIds),
'not_found_count' => \count($notFoundIds),
'total_ms' => $totalMs,
'fetch_api_ms' => $fetchMs,
'prepare_accounts_ms' => $prepareAccountsMs,
'contacts_loop_ms' => $loopMs,
'avg_contact_ms' => ! empty($allContacts) ? (int) round($loopMs / \count($allContacts)) : 0,
'slow_contacts_count' => \count($slowContacts),
'slow_contacts' => array_slice($slowContacts, 0, 10),
]);
return $result;
}
private function fetchContactsByIdsInChunks(array $crmIds): array
{
$fields = $this->getContactFields();
$allContacts = [];
foreach (array_chunk($crmIds, self::BATCH_SIZE) as $chunk) {
try {
$contacts = $this->client->getContactsByIds($chunk, $fields);
foreach ($contacts as $contactData) {
$allContacts[] = $contactData;
}
} catch (\Throwable $e) {
// @TODO what will happen if this exception is thrown
$this->logger->warning('[' . $this->getDisplayName() . '] Batch contact fetch failed', [
'chunk_size' => \count($chunk),
'error' => $e->getMessage(),
]);
throw $e;
}
}
return $allContacts;
}
private function prepareAccountMappingsForContacts(array $contacts): array
{
$companyIds = [];
foreach ($contacts as $contact) {
$companyId = $contact['properties']['associatedcompanyid'] ?? null;
if ($companyId !== null && $companyId !== '') {
$companyIds[] = (string) $companyId;
}
}
$companyIds = array_unique($companyIds);
if (empty($companyIds)) {
return [];
}
$mappings = $this->crmEntityRepository
->getExistingAccountIdsMap($this->config, $companyIds);
$missingCompanyIds = array_diff($companyIds, array_keys($mappings));
if (empty($missingCompanyIds)) {
return $mappings;
}
$this->logger->info('[' . $this->getDisplayName() . '] Batch syncing missing accounts for contacts', [
'teamId' => $this->team->getId(),
'total_companies' => \count($companyIds),
'existing_companies' => \count($mappings),
'missing_companies' => \count($missingCompanyIds),
]);
try {
$syncedAccounts = $this->batchSyncAccountsForContacts($missingCompanyIds);
$mappings = array_merge($mappings, $syncedAccounts);
} catch (\Throwable $e) {
$this->logger->error('[' . $this->getDisplayName() . '] Failed to batch sync missing accounts', [
'teamId' => $this->team->getId(),
'missingCompanyIds' => $missingCompanyIds,
'missingCount' => count($missingCompanyIds),
'error' => $e->getMessage(),
]);
}
return $mappings;
}
private function batchSyncAccountsForContacts(array $companyIds): array
{
$syncedAccounts = [];
$fields = $this->getCompanyFields();
foreach (array_chunk($companyIds, self::BATCH_SIZE) as $chunk) {
try {
$companies = $this->client->getCompaniesByIds($chunk, $fields);
foreach ($companies as $companyData) {
try {
$account = $this->importAccount($companyData);
if ($account) {
$syncedAccounts[$account->getCrmProviderId()] = $account->getId();
}
} catch (\Throwable $e) {
$this->logger->warning('[' . $this->getDisplayName() . '] Failed to import account in batch', [
'teamId' => $this->team->getId(),
'companyId' => $companyData['id'] ?? 'unknown',
'error' => $e->getMessage(),
]);
}
}
} catch (\Throwable $e) {
$this->logger->warning('[' . $this->getDisplayName() . '] Batch company fetch failed', [
'teamId' => $this->team->getId(),
'chunk_size' => \count($chunk),
'error' => $e->getMessage(),
]);
}
}
return $syncedAccounts;
}
/**
* Process webhook-collected company batches.
*
* Drains Redis sets containing company CRM IDs collected from webhook events
* and dispatches ImportAccountBatch jobs for batch processing.
*
* @return int Number of company IDs dispatched to jobs
*/
public function batchSyncCompanies(): int
{
$configId = $this->team->getCrmConfiguration()->getId();
return $this->batchProcessor->processBatchesForObjectType(
WebhookSyncBatchProcessor::OBJECT_TYPE_COMPANY,
$configId
);
}
public function importAccountBatchByIds(array $crmIds): array
{
$result = [
'success_count' => 0,
'failed_ids' => [],
'errors' => [],
];
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$batchStart = microtime(true);
$slowAccounts = [];
$fields = $this->getCompanyFields();
$allCompanies = [];
$fetchStart = microtime(true);
foreach (array_chunk($crmIds, self::BATCH_SIZE) as $chunk) {
try {
$companies = $this->client->getCompaniesByIds($chunk, $fields);
foreach ($companies as $companyData) {
$allCompanies[] = $companyData;
}
} catch (\Throwable $e) {
$this->logger->warning('[' . $this->getDisplayName() . '] Batch company fetch failed', [
'chunk_size' => \count($chunk),
'error' => $e->getMessage(),
]);
throw $e;
}
}
$fetchMs = (int) round((microtime(true) - $fetchStart) * 1000);
$fetchedIds = array_map('strval', array_column($allCompanies, 'id'));
$notFoundIds = array_values(array_diff(array_map('strval', $crmIds), $fetchedIds));
if (! empty($notFoundIds)) {
$this->logger->info('[' . $this->getDisplayName() . '] Company CRM IDs not found in HubSpot', [
'teamId' => $this->team->getId(),
'notFoundCount' => \count($notFoundIds),
'notFoundIds' => $notFoundIds,
'requestedCount' => \count($crmIds),
'fetchedCount' => \count($allCompanies),
]);
}
$loopStart = microtime(true);
foreach ($allCompanies as $companyData) {
$accountStart = microtime(true);
try {
$account = $this->importAccount($companyData);
if ($account !== null) {
$result['success_count']++;
}
} catch (\Throwable $e) {
$crmId = $companyData['id'] ?? $companyData['properties']['hs_object_id'] ?? 'unknown';
$result['failed_ids'][] = $crmId;
$result['errors'][$crmId] = $e->getMessage();
$this->logger->warning('[' . $this->getDisplayName() . '] Failed to import account', [
'crmId' => $crmId,
'error' => $e->getMessage(),
]);
}
$accountMs = (int) round((microtime(true) - $accountStart) * 1000);
if ($accountMs > 1000) {
$crmId = $companyData['id'] ?? $companyData['properties']['hs_object_id'] ?? 'unknown';
$slowAccounts[] = ['crmId' => $crmId, 'ms' => $accountMs];
}
}
$loopMs = (int) round((microtime(true) - $loopStart) * 1000);
$totalMs = (int) round((microtime(true) - $batchStart) * 1000);
$this->logger->info('[' . $this->getDisplayName() . '] importAccountBatch timing', [
'teamId' => $this->team->getId(),
'account_count' => \count($allCompanies),
'requested_count' => \count($crmIds),
'not_found_count' => \count($notFoundIds),
'total_ms' => $totalMs,
'fetch_api_ms' => $fetchMs,
'accounts_loop_ms' => $loopMs,
'avg_account_ms' => ! empty($allCompanies) ? (int) round($loopMs / \count($allCompanies)) : 0,
'slow_accounts_count' => \count($slowAccounts),
'slow_accounts' => array_slice($slowAccounts, 0, 10),
]);
return $result;
}
private function getCompanyFields(): array
{
return [
'country',
'name',
'phone',
'domain',
'hs_avatar_filemanager_key',
'industry',
'hubspot_owner_id',
'hs_object_id',
'createdate',
'hs_lastmodifieddate',
];
}
private function importAccount($crmData): ?Account
{
$crmProviderId = $crmData['id'] ?? $crmData['properties']['hs_object_id'] ?? null;
$this->logger->info('[HubSpot] importAccount', [
'crm_provider_id' => $crmProviderId,
'config_id' => $this->config->getId(),
]);
if (empty($crmData['properties'])) {
$this->logger->info('[HubSpot] importAccount failed: empty properties', [
'crm_provider_id' => $crmProviderId,
'config_id' => $this->config->getId(),
]);
return null;
}
$properties = $crmData['properties'];
$crmId = (string) $properties['hs_object_id'];
$countryCode = isset($properties['country']) ? $this->convertCountryNameToCode($properties['country']) : null;
if (isset($properties['phone'])) {
// Trim to our width and attempt to parse it.
$number = mb_strimwidth($properties['phone'], 0, 25);
$parsedNumber = parsePhoneNumber($countryCode, $number);
} else {
$parsedNumber = [];
}
$name = '[unknown]';
if (isset($properties['name'])) {
$name = $properties['name'];
}
$photoPath = $this->prospectPhotoPathService->getOrGeneratePhotoPath(
$this->config,
$crmId,
Account::class,
$crmId,
$name
);
$industry = null;
if (isset($properties['industry'])) {
$industry = mb_strimwidth($properties['industry'], 0, 40);
}
$ownerId = $profile = null;
if (isset($properties['hubspot_owner_id'])) {
$ownerId = $properties['hubspot_owner_id'];
$profile = $this->crmEntityRepository->findProfileByExternalId($this->config, (string) $ownerId);
}
$domain = null;
if (isset($properties['domain'])) {
$domain = StringUtil::resolveDomain($properties['domain']);
}
$remotelyCreatedAt = null;
if (isset($properties['createdate']) && ! empty($properties['createdate'])) {
$remotelyCreatedAt = Carbon::parse($properties['createdate']);
}
$data = [
'crm_provider_id' => $crmId,
'team_id' => $this->team->id,
'user_id' => $profile ? $profile->user_id : null,
'owner_id' => $ownerId,
'name' => mb_strimwidth($name, 0, 191),
'photo_path' => $photoPath,
'industry' => $industry,
'domain' => $domain !== null
? substr($domain, 0, 191)
: null,
'phone' => $parsedNumber['phone'] ?? null,
'ext' => $parsedNumber['ext'] ?? null,
'country_code' => $countryCode,
'remotely_created_at' => $remotelyCreatedAt,
];
return $this->crmEntityRepository->importAccount($this->config, $data);
}
public function deleteContact(string $crmProviderId): bool
{
try {
$contact = $this->crmEntityRepository->findContactByExternalId($this->config, $crmProviderId);
if (! $contact) {
$this->logger->info('[HubSpot] Contact not found for deletion', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
// return success because we do not know which instance is the target
return true;
}
$id = $contact->getId();
$this->logger->info('[HubSpot] Deleting contact via webhook', [
'contact_id' => $id,
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
$contact->delete();
DeleteContactJob::dispatch($id)->afterCommit();
return true;
} catch (Exception $e) {
$this->logger->error('[HubSpot] Failed to delete contact via webhook', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
'error' => $e->getMessage(),
]);
return false;
}
}
public function deleteAccount(string $crmProviderId): bool
{
try {
$account = $this->crmEntityRepository->findAccountByExternalId($this->config, $crmProviderId);
if (! $account) {
$this->logger->info('[HubSpot] Account not found for deletion', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
// return success because we do not know which instance is the target
return true;
}
$id = $account->getId();
$this->logger->info('[HubSpot] Deleting account via webhook', [
'account_id' => $id,
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
$account->delete();
DeleteAccountJob::dispatch($id)->afterCommit();
return true;
} catch (Exception $e) {
$this->logger->error('[HubSpot] Failed to delete account via webhook', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
'error' => $e->getMessage(),
]);
return false;
}
}
public function deleteOpportunity(string $crmProviderId): bool
{
try {
$opportunity = $this->crmEntityRepository->findOpportunityByExternalId($this->config, $crmProviderId);
if (! $opportunity) {
$this->logger->info('[HubSpot] Opportunity not found for deletion', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
// return success because we do not know which instance is the target
return true;
}
$id = $opportunity->getId();
$this->logger->info('[HubSpot] Deleting opportunity via webhook', [
'opportunity_id' => $id,
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
$opportunity->delete();
DeleteOpportunityJob::dispatch($id)->afterCommit();
return true;
} catch (Exception $e) {
$this->logger->error('[HubSpot] Failed to delete opportunity via webhook', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
'error' => $e->getMessage(),
]);
return false;
}
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide
app ~/jiminny/app, folder
.circleci, folder
.cursor, folder
.github
.sonarlint, folder
.vscode, folder
.windsurf, folder
app, sources root
Actions, folder
Component, folder
Acl, folder
ActionItems, folder
Activity, folder
ActivityAnalytics, folder
ActivitySearch, folder
AiActivityType, folder
AiAutomation, folder
AiCallScoring, folder
AskAnything, folder
Dtos, folder
Events, folder
AskAnythingPromptService.php
HistoryService.php
AskJiminnyAi, folder
AWS, folder
BillingManagement, folder
Cache, folder
CoachingFeedback, folder
Country, folder
CustomerApi, folder
Database, folder
Datadog, folder
DateTime, folder
DealInsights, folder
DealRisks, folder
ElasticSearch
Eloquent
Encoding
Encryption
ES
Faker
FeatureFlags
FFMpeg
FileSystem
Gecko
Gong
GuzzleHttp
KeyPoints
Kiosk
LanguageDetection
LiveFeed
Locks
Math
MediaPipeline
MeetingBot, folder
MobileSettings, folder
Model, folder
Notification, folder
Nudge, folder
ParagraphBreaker, folder
ParticipantSpeech, folder
PartitionedCookie, folder
PlaybackPage, folder
Playlist, folder
Prophet, folder
ProphetAi, folder
ProsperWorks, folder
Queue, folder
Job, folder
RateLimitAware.php, abstract class
RateLimitAwareWrapper.php, class
BotsQueueConstants.php
Constants.php
ProcessingQueueConstants.php
Router, folder
Saml2, folder
SCIM, folder
Seeder, folder
Sentry, folder
Serializer, folder
Settings, folder
Sidekick, folder
Slack, folder
TeamInsights, folder
TimeMemoryMapper, folder
Transcription, folder
TranscriptionSummary, folder
Twilio, folder
Uploader, folder
UrlGenerator, folder
Utility, folder
Exceptions, folder
Service, folder
BaseRateLimiter.php, class
EfficientJsonParser.php, class
ProviderRateLimiter.php, class
RateLimiterInstance.php, class
Uuid, folder
Waveform, folder
Webhooks, folder
Workflow, folder
Configuration, folder
Console, folder
Commands, folder
Activities, folder
Analytics, folder
Calendars, folder
Crm, folder
Hubspot, folder
IntegrationApp, folder
Traits, folder
AddLayoutEntities.php, class
AutologDelayedCommand.php, class
BullhornCommandAbstract.php, abstract class
BullhornPingCommand.php, class
BullhornSearchCommand.php, class
BullhornSessionCommand.php, class
CheckActivityLoggableCommand.php, final class
CleanDuplicateFieldDataCommand.php, class
FullSyncOpportunityCommand.php, class
LogActivitiesCommand.php, final class
ManageSyncStrategyCommand.php, class
MatchCrmObjectsCommand.php, class
MatchOpportunityActivitiesCommand.php, class
MigrateProvider.php, class
ProcessHubspotObjectsSyncBatches.php, class
PurgeDeletedOpportunitiesCommand.php, class
ResetGovernorLimits.php, class
SendNotLogged.php, class
SetupActivityTypeForFollowUp.php, final class
SetupCloseCrm.php, class
SetupCopperCrm.php, class
SetupCrmCommand.php, abstract class
SetupLayouts.php, class
SyncAccount.php, class
SyncContact.php, class
SyncFieldMetadata.php, class
SyncHubspotActiveDeals.php, class
SyncHubspotObjects.php, class
SyncLead.php, class
SyncObjects.php, class
SyncOpportunitiesMissingFieldDataCommand.php, class
SyncOpportunity.php, class
SyncProfileMetadata.php, class
SyncTeamMetadata.php, class
UpdateOpportunitySpecifications.php, class
DealInsights, folder
Dev, folder
AddRateLimitCommand.php, class
FixHubSpotTokens.php, class
FixMissMatchedCrmActivitiesCommand.php, final class
ImportCallsCommand.php, class
MonitorSocialAccountsState.php, class
Dialers, folder
DTOs, folder
Elasticsearch, folder
EngagementStats, folder
GeckoExport, folder
Livestream, folder
Mailboxes, folder
Migrate, folder
PlaybackThemes, folder
Playbooks, folder
Playlists, folder
Postmark, folder
ProphetAi, folder
Reports, folder
AutomatedReportsCommand.php, class
AutomatedReportsRetentionPolicyCommand.php, class
AutomatedReportsSendCommand.php, class
CreateMockAskJiminnyReportResultCommand.php, class
DeleteReportCommand.php, class
GenerateMarketingReport.php, class
Team.php, class
Usage.php, class
Slack, folder
Teams, folder
Tracks, folder
Transcription, folder
Twilio, folder
Users, folder
Vocabulary, folder
Zoom, folder
Command.php
CreateDatabaseUsers.php
DatabaseTableCount.php
DeleteOldAiCrmNotesCommand.php
DeleteS3LeftoversCommand.php
DevPostmanCommand.php
DiarizeViaAiParticipantIdentificationCommand.php
EncryptTokensCommand.php
EngagementStatsRegenerateCommand.php
FeatureFlagsHelper.php
FixCrossTenantIssues.php
FlushRolesPermissionsCache.php
GenerateInternalWebhookToken.php
GroupSetDefaultLanguageCommand.php
HelperTruncateCoachingTables.php
HubspotJournalPollingCommand.php
HubspotWebhookServiceCommand.php
ImportRecording.php
ImportUsersFromCsvFile.php
IterateUsersCommand.php
JiminnyCacheClearCommand.php
JiminnyDebugCommand.php
JiminnySetEncryptedTokenManagerModeCommand.php
JiminnyTokenInfoCommand.php
MakeSlackLiveCoachingChatNotesOn.php
ManageScimForTeam.php
MarkBranchForEnvironmentPipelineCommand.php
MuteOrganizerChannel.php, class
PhpApm.php, class
PropagateCoachingFeedbackCreatedAtToSectionFeedbacks.php, class
PurgeConferences.php, class
PurgeSoftDeletedOpportunitiesCommand.php, class
PurgeSyncBatchesCommand.php, class
RecalculateDealRisksCommand.php, class
RemoveDeleteMarkersCommand.php
RemoveExpiredNudgesCommand.php
RemoveUnusedParticipantSpeechesCommand.php
ResetElasticSearch.php
RestoreActivityCrmProviderIdCommand.php
RestoreActivityTypeCommand.php
RunAiCallScoringForUntypedActivitiesCommand.php
SeedActivities.php
SendNudgeExpirationWarningsCommand.php
SyncActivity.php, class...
|
2842
|
NULL
|
NULL
|
NULL
|
|
2843
|
114
|
36
|
2026-05-07T11:43:04.407014+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778154184407_m2.jpg...
|
PhpStorm
|
faVsco.js – SyncCrmEntitiesTrait.php
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
Editor for custom.log
Sync Changes
Hide This Notification
Code changed:
Hide
Analyzing…
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\ServiceTraits;
use Carbon\Carbon;
use Exception;
use Illuminate\Support\Str;
use Jiminny\Exceptions\CrmException;
use Jiminny\Jobs\Crm\Delete\DeleteAccountJob;
use Jiminny\Jobs\Crm\Delete\DeleteContactJob;
use Jiminny\Jobs\Crm\Delete\DeleteOpportunityJob;
use Jiminny\Models\Account;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Repositories\Crm\CrmEntityRepository;
use Jiminny\Services\Avatar\ProspectPhotoPathService;
use Jiminny\Services\Crm\Hubspot\HubspotClientInterface;
use Jiminny\Services\Crm\Hubspot\WebhookSyncBatchProcessor;
use Jiminny\Utils\StringUtil;
trait SyncCrmEntitiesTrait
{
use OpportunitySyncTrait;
private const string CDN_URL = '[URL_WITH_CREDENTIALS] Carbon $since Fetch contacts modified after this date
* @param Carbon|null $to Optional end date for modification range
*
* @return int Number of contacts successfully synced
*/
public function syncContacts(Carbon $since, ?Carbon $to = null): int
{
$syncCount = 0;
try {
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$fields = $this->getContactFields();
$payload = $this->payloadBuilder->getRecentlyUpdatedSearchPayload($since, $to, $fields);
foreach ($this->client->getPaginatedDataGenerator($payload, 'contacts') as $hsContact) {
$this->importContact($hsContact);
$syncCount++;
}
} catch (Exception $exception) {
$this->logger->error('[' . $this->getDisplayName() . '] Sync contacts failed', [
'teamId' => $this->team->getUuid(),
'reason' => $exception->getMessage(),
]);
}
return $syncCount;
}
/**
* @inheritdoc
*/
public function syncContact(string $crmId): ?Contact
{
try {
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$fields = $this->getContactFields();
$hsContact = $this->client->getContactById($crmId, $fields);
} catch (\HubSpot\Client\Crm\Contacts\ApiException $e) {
$this->logger->info('[' . $this->getDisplayName() . '] Contact fetch failed', [
'teamId' => $this->team->getUuid(),
'crmId' => $crmId,
'reason' => $e->getMessage(),
]);
return null;
} catch (CrmException $e) {
$this->logger->info('[' . $this->getDisplayName() . '] Contact not found', [
'teamId' => $this->team->getUuid(),
'crmId' => $crmId,
'reason' => $e->getMessage(),
]);
return null;
}
if (empty($hsContact['properties']) || empty($hsContact['id'])) {
$this->logger->warning('[' . $this->getDisplayName() . '] Contact data incomplete', [
'teamId' => $this->team->getUuid(),
'crmId' => $crmId,
'has_properties' => ! empty($hsContact['properties']),
'has_id' => ! empty($hsContact['id']),
]);
return null;
}
return $this->importContact($hsContact);
}
private function getContactFields(): array
{
return [
'associatedcompanyid',
'country',
'firstname',
'lastname',
'phone',
'mobilephone',
'email',
'photo',
'hs_avatar_filemanager_key',
'jobtitle',
'hubspot_owner_id',
'hs_object_id',
'createdate',
'hs_lastmodifieddate',
];
}
/**
* @inheritdoc
*/
private function importContact($crmData, array $accountMappings = []): ?Contact
{
$crmProviderId = $crmData['id'] ?? null;
$this->logger->info('[HubSpot] importContact', [
'crm_provider_id' => $crmProviderId,
'config_id' => $this->config->getId(),
]);
if (empty($crmData['properties'])) {
$this->logger->info('[HubSpot] importContact failed: empty properties', [
'crm_provider_id' => $crmProviderId,
'config_id' => $this->config->getId(),
]);
return null;
}
$properties = $crmData['properties'];
$crmId = (string) $crmData['id'];
$accountId = $this->resolveContactAccount($properties, $accountMappings);
$data = $this->buildContactData($crmId, $properties, $accountId);
return $this->crmEntityRepository->importContact($this->config, $data);
}
private function resolveContactAccount(array $properties, array $accountMappings): ?int
{
if (empty($properties['associatedcompanyid'])) {
return null;
}
$companyId = (string) $properties['associatedcompanyid'];
if (! empty($accountMappings)) {
return $accountMappings[$companyId] ?? null;
}
return $this->crmEntityRepository->findAccountByExternalId(
$this->team->getCrmConfiguration(),
$companyId
)?->getId() ?? $this->syncAccount($companyId)?->getId();
}
private function buildContactData(string $crmId, array $properties, ?int $accountId): array
{
$countryCode = $this->buildContactCountry($properties);
$name = $this->buildContactName($properties);
$photoPath = $this->teamService->generateAvatar(
$crmId,
empty($name) ? ($properties['email'] ?? 'N/A') : $name,
);
$parsedNumber = $this->buildContactPhone($countryCode, $properties);
$mobileNumber = $this->buildContactMobilePhone($countryCode, $properties);
$ownerId = $properties['hubspot_owner_id'] ?? null;
$profile = $ownerId !== null
? $this->crmEntityRepository->findProfileByExternalId($this->config, (string) $ownerId)
: null;
$ext = (isset($parsedNumber['ext']) && is_string($parsedNumber['ext']) && strlen($parsedNumber['ext']) <= 10)
? $parsedNumber['ext']
: null;
$title = isset($properties['jobtitle']) ? mb_strimwidth($properties['jobtitle'], 0, 128) : null;
$email = isset($properties['email']) ? mb_strimwidth($properties['email'], 0, 191) : null;
$remotelyCreatedAt = ! empty($properties['createdate']) ? Carbon::parse($properties['createdate']) : null;
return [
'crm_provider_id' => $crmId,
'team_id' => $this->team->getId(),
'account_id' => $accountId,
'user_id' => $profile?->getUserId(),
'owner_id' => $ownerId,
'name' => $name,
'title' => $title,
'email' => $email,
'country_code' => $countryCode,
'phone' => $parsedNumber['phone'] ?? null,
'mobile_phone' => $mobileNumber ?? null,
'ext' => $ext,
'photo_path' => $photoPath,
'remotely_created_at' => $remotelyCreatedAt,
];
}
/**
* @param $properties
*/
private function buildContactName($properties): string
{
if (is_array($properties)) {
return $this->buildContactNameFromArray($properties);
}
return $this->buildContactNameFromObject($properties);
}
private function buildContactNameFromArray(array $properties): string
{
if (! empty($properties['name'])) {
return mb_strimwidth($properties['name'], 0, 100);
}
$name = '';
if (! empty($properties['firstname'])) {
$name = $properties['firstname'] . ' ';
}
if (! empty($properties['lastname'])) {
$name .= $properties['lastname'];
}
if ($name === '' && ! empty($properties['email'])) {
$name = $properties['email'];
}
return mb_strimwidth($name, 0, 100);
}
private function buildContactNameFromObject($properties): string
{
$name = '';
if (isset($properties->firstname)) {
$name = $properties->firstname->value . ' ';
}
if (isset($properties->lastname)) {
$name .= $properties->lastname->value;
}
if ($name === '' && isset($properties->email)) {
$name = $properties->email->value;
}
return mb_strimwidth($name, 0, 100);
}
/**
* @param $properties
*/
private function buildContactPhone(?string $countryCode, $properties): ?array
{
if (is_array($properties) && empty($properties['phone']) === false) {
$number = mb_strimwidth($properties['phone'], 0, 25);
return parsePhoneNumber($countryCode, $number);
} elseif (isset($properties->phone)) {
$number = mb_strimwidth($properties->phone->value, 0, 25);
return parsePhoneNumber($countryCode, $number);
}
return [];
}
/**
* @param $properties
*/
private function buildContactMobilePhone(?string $countryCode, $properties): ?string
{
return isset($properties['mobilephone'])
? Str::limit(phone_e164($countryCode, $properties['mobilephone']), 25, '')
: null;
}
/**
* @param $properties
* @param $account
*/
private function buildContactCountry($properties): ?string
{
if (is_array($properties) && empty($properties['country']) === false) {
return $this->convertCountryNameToCode($properties['country']);
}
if (isset($properties->country)) {
return $this->convertCountryNameToCode($properties->country->value);
}
return null;
}
/**
* HubSpot doesn't have leads, so this method does nothing.
*
* @param Carbon $since
* @param Carbon|null $to
* @param string|null $crmProfileId
*
* @return int
*/
public function syncLeads(Carbon $since, ?Carbon $to = null, ?string $crmProfileId = null): int
{
// Mark unused parameters to avoid code smell warnings
unset($since, $to, $crmProfileId);
return 0;
}
/**
* HubSpot doesn't have leads.
*
* @param string $crmId
*
* @inheritdoc
*/
public function syncLead(string $crmId): ?Lead
{
// Mark unused parameter to avoid code smell warnings
unset($crmId);
return null;
}
/**
* Sync accounts (companies) modified since a given date (manual sync mode).
*
* This method fetches companies from HubSpot API based on modification date and
* imports them one by one. It is used for:
* - Manual sync commands (e.g., crm:sync-account with --from parameter)
* - Initial sync for new teams
* - Backfill operations
*
* For regular sync webhook batchSyncCompanies is used:
*
* @param Carbon $since Fetch companies modified after this date
* @param Carbon|null $to Optional end date for modification range
*
* @return int Number of companies successfully synced
*/
public function syncAccounts(Carbon $since, ?Carbon $to = null): int
{
$syncCount = 0;
try {
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$fields = $this->getCompanyFields();
$payload = $this->payloadBuilder->getRecentlyUpdatedSearchPayload($since, $to, $fields);
foreach ($this->client->getPaginatedDataGenerator($payload, 'companies') as $hsAccount) {
$this->importAccount($hsAccount);
$syncCount++;
}
} catch (Exception $exception) {
$this->logger->error('[' . $this->getDisplayName() . '] Sync accounts failed', [
'teamId' => $this->team->getUuid(),
'reason' => $exception->getMessage(),
]);
}
return $syncCount;
}
/**
* @inheritdoc
*/
public function syncAccount(string $crmId): ?Account
{
try {
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$fields = $this->getCompanyFields();
$hsAccount = $this->client->getAccountById($crmId, $fields);
} catch (\HubSpot\Client\Crm\Companies\ApiException $e) {
$this->logger->info('[' . $this->getDisplayName() . '] Account fetch failed', [
'teamId' => $this->team->getUuid(),
'crmId' => $crmId,
'reason' => $e->getMessage(),
]);
return null;
} catch (CrmException $e) {
$this->logger->info('[' . $this->getDisplayName() . '] Account not found', [
'teamId' => $this->team->getUuid(),
'crmId' => $crmId,
'reason' => $e->getMessage(),
]);
return null;
}
return $this->importAccount($hsAccount);
}
/**
* Process webhook-collected contact batches.
*
* Drains Redis sets containing contact CRM IDs collected from webhook events
* and dispatches ImportContactBatch jobs for batch processing.
*
* @return int Number of contact IDs dispatched to jobs
*/
public function batchSyncContacts(): int
{
$configId = $this->team->getCrmConfiguration()->getId();
return $this->batchProcessor->processBatchesForObjectType(
WebhookSyncBatchProcessor::OBJECT_TYPE_CONTACT,
$configId
);
}
public function importContactBatchByIds(array $crmIds): array
{
$result = [
'success_count' => 0,
'failed_ids' => [],
'errors' => [],
];
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$batchStart = microtime(true);
$slowContacts = [];
$fetchStart = microtime(true);
$allContacts = $this->fetchContactsByIdsInChunks($crmIds);
$fetchMs = (int) round((microtime(true) - $fetchStart) * 1000);
$fetchedIds = array_map('strval', array_column($allContacts, 'id'));
$notFoundIds = array_values(array_diff(array_map('strval', $crmIds), $fetchedIds));
if (! empty($notFoundIds)) {
$this->logger->info('[' . $this->getDisplayName() . '] Contact CRM IDs not found in HubSpot', [
'teamId' => $this->team->getId(),
'notFoundCount' => \count($notFoundIds),
'notFoundIds' => $notFoundIds,
'requestedCount' => \count($crmIds),
'fetchedCount' => \count($allContacts),
]);
}
if (empty($allContacts)) {
return $result;
}
$prepareStart = microtime(true);
$accountMappings = $this->prepareAccountMappingsForContacts($allContacts);
$prepareAccountsMs = (int) round((microtime(true) - $prepareStart) * 1000);
$loopStart = microtime(true);
foreach ($allContacts as $contactData) {
$contactStart = microtime(true);
try {
$contact = $this->importContact($contactData, $accountMappings);
if ($contact !== null) {
$result['success_count']++;
}
} catch (\Throwable $e) {
$crmId = $contactData['id'] ?? 'unknown';
$result['failed_ids'][] = $crmId;
$result['errors'][$crmId] = $e->getMessage();
$this->logger->warning('[' . $this->getDisplayName() . '] Failed to import contact', [
'teamId' => $this->team->getId(),
'crmId' => $crmId,
'error' => $e->getMessage(),
]);
}
$contactMs = (int) round((microtime(true) - $contactStart) * 1000);
if ($contactMs > 1000) {
$slowContacts[] = ['crmId' => $contactData['id'] ?? 'unknown', 'ms' => $contactMs];
}
}
$loopMs = (int) round((microtime(true) - $loopStart) * 1000);
$totalMs = (int) round((microtime(true) - $batchStart) * 1000);
$this->logger->info('[' . $this->getDisplayName() . '] importContactBatch timing', [
'teamId' => $this->team->getId(),
'contact_count' => \count($allContacts),
'requested_count' => \count($crmIds),
'not_found_count' => \count($notFoundIds),
'total_ms' => $totalMs,
'fetch_api_ms' => $fetchMs,
'prepare_accounts_ms' => $prepareAccountsMs,
'contacts_loop_ms' => $loopMs,
'avg_contact_ms' => ! empty($allContacts) ? (int) round($loopMs / \count($allContacts)) : 0,
'slow_contacts_count' => \count($slowContacts),
'slow_contacts' => array_slice($slowContacts, 0, 10),
]);
return $result;
}
private function fetchContactsByIdsInChunks(array $crmIds): array
{
$fields = $this->getContactFields();
$allContacts = [];
foreach (array_chunk($crmIds, self::BATCH_SIZE) as $chunk) {
try {
$contacts = $this->client->getContactsByIds($chunk, $fields);
foreach ($contacts as $contactData) {
$allContacts[] = $contactData;
}
} catch (\Throwable $e) {
// @TODO what will happen if this exception is thrown
$this->logger->warning('[' . $this->getDisplayName() . '] Batch contact fetch failed', [
'chunk_size' => \count($chunk),
'error' => $e->getMessage(),
]);
throw $e;
}
}
return $allContacts;
}
private function prepareAccountMappingsForContacts(array $contacts): array
{
$companyIds = [];
foreach ($contacts as $contact) {
$companyId = $contact['properties']['associatedcompanyid'] ?? null;
if ($companyId !== null && $companyId !== '') {
$companyIds[] = (string) $companyId;
}
}
$companyIds = array_unique($companyIds);
if (empty($companyIds)) {
return [];
}
$mappings = $this->crmEntityRepository
->getExistingAccountIdsMap($this->config, $companyIds);
$missingCompanyIds = array_diff($companyIds, array_keys($mappings));
if (empty($missingCompanyIds)) {
return $mappings;
}
$this->logger->info('[' . $this->getDisplayName() . '] Batch syncing missing accounts for contacts', [
'teamId' => $this->team->getId(),
'total_companies' => \count($companyIds),
'existing_companies' => \count($mappings),
'missing_companies' => \count($missingCompanyIds),
]);
try {
$syncedAccounts = $this->batchSyncAccountsForContacts($missingCompanyIds);
$mappings = array_merge($mappings, $syncedAccounts);
} catch (\Throwable $e) {
$this->logger->error('[' . $this->getDisplayName() . '] Failed to batch sync missing accounts', [
'teamId' => $this->team->getId(),
'missingCompanyIds' => $missingCompanyIds,
'missingCount' => count($missingCompanyIds),
'error' => $e->getMessage(),
]);
}
return $mappings;
}
private function batchSyncAccountsForContacts(array $companyIds): array
{
$syncedAccounts = [];
$fields = $this->getCompanyFields();
foreach (array_chunk($companyIds, self::BATCH_SIZE) as $chunk) {
try {
$companies = $this->client->getCompaniesByIds($chunk, $fields);
foreach ($companies as $companyData) {
try {
$account = $this->importAccount($companyData);
if ($account) {
$syncedAccounts[$account->getCrmProviderId()] = $account->getId();
}
} catch (\Throwable $e) {
$this->logger->warning('[' . $this->getDisplayName() . '] Failed to import account in batch', [
'teamId' => $this->team->getId(),
'companyId' => $companyData['id'] ?? 'unknown',
'error' => $e->getMessage(),
]);
}
}
} catch (\Throwable $e) {
$this->logger->warning('[' . $this->getDisplayName() . '] Batch company fetch failed', [
'teamId' => $this->team->getId(),
'chunk_size' => \count($chunk),
'error' => $e->getMessage(),
]);
}
}
return $syncedAccounts;
}
/**
* Process webhook-collected company batches.
*
* Drains Redis sets containing company CRM IDs collected from webhook events
* and dispatches ImportAccountBatch jobs for batch processing.
*
* @return int Number of company IDs dispatched to jobs
*/
public function batchSyncCompanies(): int
{
$configId = $this->team->getCrmConfiguration()->getId();
return $this->batchProcessor->processBatchesForObjectType(
WebhookSyncBatchProcessor::OBJECT_TYPE_COMPANY,
$configId
);
}
public function importAccountBatchByIds(array $crmIds): array
{
$result = [
'success_count' => 0,
'failed_ids' => [],
'errors' => [],
];
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$batchStart = microtime(true);
$slowAccounts = [];
$fields = $this->getCompanyFields();
$allCompanies = [];
$fetchStart = microtime(true);
foreach (array_chunk($crmIds, self::BATCH_SIZE) as $chunk) {
try {
$companies = $this->client->getCompaniesByIds($chunk, $fields);
foreach ($companies as $companyData) {
$allCompanies[] = $companyData;
}
} catch (\Throwable $e) {
$this->logger->warning('[' . $this->getDisplayName() . '] Batch company fetch failed', [
'chunk_size' => \count($chunk),
'error' => $e->getMessage(),
]);
throw $e;
}
}
$fetchMs = (int) round((microtime(true) - $fetchStart) * 1000);
$fetchedIds = array_map('strval', array_column($allCompanies, 'id'));
$notFoundIds = array_values(array_diff(array_map('strval', $crmIds), $fetchedIds));
if (! empty($notFoundIds)) {
$this->logger->info('[' . $this->getDisplayName() . '] Company CRM IDs not found in HubSpot', [
'teamId' => $this->team->getId(),
'notFoundCount' => \count($notFoundIds),
'notFoundIds' => $notFoundIds,
'requestedCount' => \count($crmIds),
'fetchedCount' => \count($allCompanies),
]);
}
$loopStart = microtime(true);
foreach ($allCompanies as $companyData) {
$accountStart = microtime(true);
try {
$account = $this->importAccount($companyData);
if ($account !== null) {
$result['success_count']++;
}
} catch (\Throwable $e) {
$crmId = $companyData['id'] ?? $companyData['properties']['hs_object_id'] ?? 'unknown';
$result['failed_ids'][] = $crmId;
$result['errors'][$crmId] = $e->getMessage();
$this->logger->warning('[' . $this->getDisplayName() . '] Failed to import account', [
'crmId' => $crmId,
'error' => $e->getMessage(),
]);
}
$accountMs = (int) round((microtime(true) - $accountStart) * 1000);
if ($accountMs > 1000) {
$crmId = $companyData['id'] ?? $companyData['properties']['hs_object_id'] ?? 'unknown';
$slowAccounts[] = ['crmId' => $crmId, 'ms' => $accountMs];
}
}
$loopMs = (int) round((microtime(true) - $loopStart) * 1000);
$totalMs = (int) round((microtime(true) - $batchStart) * 1000);
$this->logger->info('[' . $this->getDisplayName() . '] importAccountBatch timing', [
'teamId' => $this->team->getId(),
'account_count' => \count($allCompanies),
'requested_count' => \count($crmIds),
'not_found_count' => \count($notFoundIds),
'total_ms' => $totalMs,
'fetch_api_ms' => $fetchMs,
'accounts_loop_ms' => $loopMs,
'avg_account_ms' => ! empty($allCompanies) ? (int) round($loopMs / \count($allCompanies)) : 0,
'slow_accounts_count' => \count($slowAccounts),
'slow_accounts' => array_slice($slowAccounts, 0, 10),
]);
return $result;
}
private function getCompanyFields(): array
{
return [
'country',
'name',
'phone',
'domain',
'hs_avatar_filemanager_key',
'industry',
'hubspot_owner_id',
'hs_object_id',
'createdate',
'hs_lastmodifieddate',
];
}
private function importAccount($crmData): ?Account
{
$crmProviderId = $crmData['id'] ?? $crmData['properties']['hs_object_id'] ?? null;
$this->logger->info('[HubSpot] importAccount', [
'crm_provider_id' => $crmProviderId,
'config_id' => $this->config->getId(),
]);
if (empty($crmData['properties'])) {
$this->logger->info('[HubSpot] importAccount failed: empty properties', [
'crm_provider_id' => $crmProviderId,
'config_id' => $this->config->getId(),
]);
return null;
}
$properties = $crmData['properties'];
$crmId = (string) $properties['hs_object_id'];
$countryCode = isset($properties['country']) ? $this->convertCountryNameToCode($properties['country']) : null;
if (isset($properties['phone'])) {
// Trim to our width and attempt to parse it.
$number = mb_strimwidth($properties['phone'], 0, 25);
$parsedNumber = parsePhoneNumber($countryCode, $number);
} else {
$parsedNumber = [];
}
$name = '[unknown]';
if (isset($properties['name'])) {
$name = $properties['name'];
}
$photoPath = $this->prospectPhotoPathService->getOrGeneratePhotoPath(
$this->config,
$crmId,
Account::class,
$crmId,
$name
);
$industry = null;
if (isset($properties['industry'])) {
$industry = mb_strimwidth($properties['industry'], 0, 40);
}
$ownerId = $profile = null;
if (isset($properties['hubspot_owner_id'])) {
$ownerId = $properties['hubspot_owner_id'];
$profile = $this->crmEntityRepository->findProfileByExternalId($this->config, (string) $ownerId);
}
$domain = null;
if (isset($properties['domain'])) {
$domain = StringUtil::resolveDomain($properties['domain']);
}
$remotelyCreatedAt = null;
if (isset($properties['createdate']) && ! empty($properties['createdate'])) {
$remotelyCreatedAt = Carbon::parse($properties['createdate']);
}
$data = [
'crm_provider_id' => $crmId,
'team_id' => $this->team->id,
'user_id' => $profile ? $profile->user_id : null,
'owner_id' => $ownerId,
'name' => mb_strimwidth($name, 0, 191),
'photo_path' => $photoPath,
'industry' => $industry,
'domain' => $domain !== null
? substr($domain, 0, 191)
: null,
'phone' => $parsedNumber['phone'] ?? null,
'ext' => $parsedNumber['ext'] ?? null,
'country_code' => $countryCode,
'remotely_created_at' => $remotelyCreatedAt,
];
return $this->crmEntityRepository->importAccount($this->config, $data);
}
public function deleteContact(string $crmProviderId): bool
{
try {
$contact = $this->crmEntityRepository->findContactByExternalId($this->config, $crmProviderId);
if (! $contact) {
$this->logger->info('[HubSpot] Contact not found for deletion', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
// return success because we do not know which instance is the target
return true;
}
$id = $contact->getId();
$this->logger->info('[HubSpot] Deleting contact via webhook', [
'contact_id' => $id,
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
$contact->delete();
DeleteContactJob::dispatch($id)->afterCommit();
return true;
} catch (Exception $e) {
$this->logger->error('[HubSpot] Failed to delete contact via webhook', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
'error' => $e->getMessage(),
]);
return false;
}
}
public function deleteAccount(string $crmProviderId): bool
{
try {
$account = $this->crmEntityRepository->findAccountByExternalId($this->config, $crmProviderId);
if (! $account) {
$this->logger->info('[HubSpot] Account not found for deletion', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
// return success because we do not know which instance is the target
return true;
}
$id = $account->getId();
$this->logger->info('[HubSpot] Deleting account via webhook', [
'account_id' => $id,
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
$account->delete();
DeleteAccountJob::dispatch($id)->afterCommit();
return true;
} catch (Exception $e) {
$this->logger->error('[HubSpot] Failed to delete account via webhook', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
'error' => $e->getMessage(),
]);
return false;
}
}
public function deleteOpportunity(string $crmProviderId): bool
{
try {
$opportunity = $this->crmEntityRepository->findOpportunityByExternalId($this->config, $crmProviderId);
if (! $opportunity) {
$this->logger->info('[HubSpot] Opportunity not found for deletion', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
// return success because we do not know which instance is the target
return true;
}
$id = $opportunity->getId();
$this->logger->info('[HubSpot] Deleting opportunity via webhook', [
'opportunity_id' => $id,
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
$opportunity->delete();
DeleteOpportunityJob::dispatch($id)->afterCommit();
return true;
} catch (Exception $e) {
$this->logger->error('[HubSpot] Failed to delete opportunity via webhook', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
'error' => $e->getMessage(),
]);
return false;
}
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide
app ~/jiminny/app, folder
.circleci, folder
.cursor, folder
.github
.sonarlint, folder
.vscode, folder
.windsurf, folder
app, sources root
Actions, folder
Component, folder
Acl, folder
ActionItems, folder
Activity, folder
ActivityAnalytics, folder
ActivitySearch, folder
AiActivityType, folder
AiAutomation, folder
AiCallScoring, folder
AskAnything, folder
Dtos, folder
Events, folder
AskAnythingPromptService.php
HistoryService.php
AskJiminnyAi, folder
AWS, folder
BillingManagement, folder
Cache, folder
CoachingFeedback, folder
Country, folder
CustomerApi, folder
Database, folder
Datadog, folder
DateTime, folder
DealInsights, folder
DealRisks, folder
ElasticSearch
Eloquent
Encoding
Encryption
ES
Faker
FeatureFlags
FFMpeg
FileSystem
Gecko
Gong
GuzzleHttp
KeyPoints
Kiosk
LanguageDetection
LiveFeed
Locks
Math
MediaPipeline
MeetingBot, folder
MobileSettings, folder
Model, folder
Notification, folder
Nudge, folder
ParagraphBreaker, folder
ParticipantSpeech, folder
PartitionedCookie, folder
PlaybackPage, folder
Playlist, folder
Prophet, folder
ProphetAi, folder
ProsperWorks, folder
Queue, folder
Job, folder
RateLimitAware.php, abstract class
RateLimitAwareWrapper.php, class
BotsQueueConstants.php
Constants.php
ProcessingQueueConstants.php
Router, folder
Saml2, folder
SCIM, folder
Seeder, folder
Sentry, folder
Serializer, folder
Settings, folder
Sidekick, folder
Slack, folder
TeamInsights, folder
TimeMemoryMapper, folder
Transcription, folder
TranscriptionSummary, folder
Twilio, folder
Uploader, folder
UrlGenerator, folder
Utility, folder
Exceptions, folder
Service, folder
BaseRateLimiter.php, class
EfficientJsonParser.php, class
ProviderRateLimiter.php, class
RateLimiterInstance.php, class
Uuid, folder
Waveform, folder
Webhooks, folder
Workflow, folder
Configuration, folder
Console, folder
Commands, folder
Activities, folder
Analytics, folder
Calendars, folder
Crm, folder
Hubspot, folder
IntegrationApp, folder
Traits, folder
AddLayoutEntities.php, class
AutologDelayedCommand.php, class
BullhornCommandAbstract.php, abstract class
BullhornPingCommand.php, class
BullhornSearchCommand.php, class
BullhornSessionCommand.php, class
CheckActivityLoggableCommand.php, final class
CleanDuplicateFieldDataCommand.php, class
FullSyncOpportunityCommand.php, class
LogActivitiesCommand.php, final class
ManageSyncStrategyCommand.php, class
MatchCrmObjectsCommand.php, class
MatchOpportunityActivitiesCommand.php, class
MigrateProvider.php, class
ProcessHubspotObjectsSyncBatches.php, class
PurgeDeletedOpportunitiesCommand.php, class
ResetGovernorLimits.php, class
SendNotLogged.php, class
SetupActivityTypeForFollowUp.php, final class
SetupCloseCrm.php, class
SetupCopperCrm.php, class
SetupCrmCommand.php, abstract class
SetupLayouts.php, class
SyncAccount.php, class
SyncContact.php, class
SyncFieldMetadata.php, class
SyncHubspotActiveDeals.php, class
SyncHubspotObjects.php, class
SyncLead.php, class
SyncObjects.php, class
SyncOpportunitiesMissingFieldDataCommand.php, class
SyncOpportunity.php, class
SyncProfileMetadata.php, class
SyncTeamMetadata.php, class
UpdateOpportunitySpecifications.php, class
DealInsights, folder
Dev, folder
AddRateLimitCommand.php, class
FixHubSpotTokens.php, class
FixMissMatchedCrmActivitiesCommand.php, final class
ImportCallsCommand.php, class
MonitorSocialAccountsState.php, class
Dialers, folder
DTOs, folder
Elasticsearch, folder
EngagementStats, folder
GeckoExport, folder
Livestream, folder
Mailboxes, folder
Migrate, folder
PlaybackThemes, folder
Playbooks, folder
Playlists, folder
Postmark, folder
ProphetAi, folder
Reports, folder
AutomatedReportsCommand.php, class
AutomatedReportsRetentionPolicyCommand.php, class
AutomatedReportsSendCommand.php, class
CreateMockAskJiminnyReportResultCommand.php, class
DeleteReportCommand.php, class
GenerateMarketingReport.php, class
Team.php, class
Usage.php, class
Slack, folder
Teams, folder
Tracks, folder
Transcription, folder
Twilio, folder
Users, folder
Vocabulary, folder
Zoom, folder
Command.php
CreateDatabaseUsers.php
DatabaseTableCount.php
DeleteOldAiCrmNotesCommand.php
DeleteS3LeftoversCommand.php
DevPostmanCommand.php
DiarizeViaAiParticipantIdentificationCommand.php
EncryptTokensCommand.php
EngagementStatsRegenerateCommand.php
FeatureFlagsHelper.php
FixCrossTenantIssues.php
FlushRolesPermissionsCache.php
GenerateInternalWebhookToken.php
GroupSetDefaultLanguageCommand.php
HelperTruncateCoachingTables.php
HubspotJournalPollingCommand.php
HubspotWebhookServiceCommand.php
ImportRecording.php
ImportUsersFromCsvFile.php
IterateUsersCommand.php
JiminnyCacheClearCommand.php
JiminnyDebugCommand.php
JiminnySetEncryptedTokenManagerModeCommand.php
JiminnyTokenInfoCommand.php
MakeSlackLiveCoachingChatNotesOn.php
ManageScimForTeam.php
MarkBranchForEnvironmentPipelineCommand.php
MuteOrganizerChannel.php, class
PhpApm.php, class
PropagateCoachingFeedbackCreatedAtToSectionFeedbacks.php, class
PurgeConferences.php, class
PurgeSoftDeletedOpportunitiesCommand.php, class
PurgeSyncBatchesCommand.php, class
RecalculateDealRisksCommand.php, class
RemoveDeleteMarkersCommand.php
RemoveExpiredNudgesCommand.php
RemoveUnusedParticipantSpeechesCommand.php
ResetElasticSearch.php
RestoreActivityCrmProviderIdCommand.php
RestoreActivityTypeCommand.php
RunAiCallScoringForUntypedActivitiesCommand.php
SeedActivities.php
SendNudgeExpirationWarningsCommand.php
SyncActivity.php, class
TrackImported.php
WhichWorkerIsWorkingOnWhichJob.php
Scheduling, folder
Kernel.php
Contracts, folder
Acl, folder
ActivitySearch, folder
AiAutomation, folder
Crm, folder
DateTime, folder
ES, folder
Http, folder
Requests, folder
ApiResponse.php
RateLimited.php
RateLimitInterface.php
Interactions, folder
Model, folder
Nudge, folder
Playlist, folder
Repositories, folder
Services, folder
User, folder...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.034242023,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: master","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"Editor for custom.log","depth":4,"bounds":{"left":0.5475399,"top":0.0726257,"width":0.44082448,"height":0.9066241},"on_screen":true,"role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Analyzing…","depth":4,"bounds":{"left":0.5053192,"top":0.17478053,"width":0.019946808,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits;\n\nuse Carbon\\Carbon;\nuse Exception;\nuse Illuminate\\Support\\Str;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Jobs\\Crm\\Delete\\DeleteAccountJob;\nuse Jiminny\\Jobs\\Crm\\Delete\\DeleteContactJob;\nuse Jiminny\\Jobs\\Crm\\Delete\\DeleteOpportunityJob;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Repositories\\Crm\\CrmEntityRepository;\nuse Jiminny\\Services\\Avatar\\ProspectPhotoPathService;\nuse Jiminny\\Services\\Crm\\Hubspot\\HubspotClientInterface;\nuse Jiminny\\Services\\Crm\\Hubspot\\WebhookSyncBatchProcessor;\nuse Jiminny\\Utils\\StringUtil;\n\ntrait SyncCrmEntitiesTrait\n{\n use OpportunitySyncTrait;\n private const string CDN_URL = 'https://cdn2.hubspot.net/';\n protected CrmEntityRepository $crmEntityRepository;\n protected ProspectPhotoPathService $prospectPhotoPathService;\n\n private function getAssociationDataForCollection(array $collection, string $fromObject, string $toObject): array\n {\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $hsOpportunityIds = array_column($collection, 'id');\n\n return $this->client->getAssociationsData($hsOpportunityIds, $fromObject, $toObject);\n }\n\n private function importAssociationData(array $collection, array $associatedData): array\n {\n $data = [];\n if (! empty($associatedData[$collection['id']])) {\n foreach ($associatedData[$collection['id']] as $id) {\n $data[] = [\n 'id' => $id,\n ];\n }\n }\n\n return ['results' => $data];\n }\n\n /**\n * Sync contacts modified since a given date (manual sync mode).\n *\n * This method fetches contacts from HubSpot API based on modification date and\n * imports them one by one. It is used for:\n * - Manual sync commands (e.g., crm:sync-contact with --from parameter)\n * - Initial sync for new teams\n * - Backfill operations\n *\n * For regular sync webhook batchSyncContacts is used:\n *\n * @param Carbon $since Fetch contacts modified after this date\n * @param Carbon|null $to Optional end date for modification range\n *\n * @return int Number of contacts successfully synced\n */\n public function syncContacts(Carbon $since, ?Carbon $to = null): int\n {\n $syncCount = 0;\n\n try {\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $fields = $this->getContactFields();\n $payload = $this->payloadBuilder->getRecentlyUpdatedSearchPayload($since, $to, $fields);\n\n foreach ($this->client->getPaginatedDataGenerator($payload, 'contacts') as $hsContact) {\n $this->importContact($hsContact);\n $syncCount++;\n }\n } catch (Exception $exception) {\n $this->logger->error('[' . $this->getDisplayName() . '] Sync contacts failed', [\n 'teamId' => $this->team->getUuid(),\n 'reason' => $exception->getMessage(),\n ]);\n }\n\n return $syncCount;\n }\n\n /**\n * @inheritdoc\n */\n public function syncContact(string $crmId): ?Contact\n {\n try {\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $fields = $this->getContactFields();\n $hsContact = $this->client->getContactById($crmId, $fields);\n } catch (\\HubSpot\\Client\\Crm\\Contacts\\ApiException $e) {\n $this->logger->info('[' . $this->getDisplayName() . '] Contact fetch failed', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n return null;\n } catch (CrmException $e) {\n $this->logger->info('[' . $this->getDisplayName() . '] Contact not found', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n return null;\n }\n\n if (empty($hsContact['properties']) || empty($hsContact['id'])) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Contact data incomplete', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'has_properties' => ! empty($hsContact['properties']),\n 'has_id' => ! empty($hsContact['id']),\n ]);\n\n return null;\n }\n\n return $this->importContact($hsContact);\n }\n\n private function getContactFields(): array\n {\n return [\n 'associatedcompanyid',\n 'country',\n 'firstname',\n 'lastname',\n 'phone',\n 'mobilephone',\n 'email',\n 'photo',\n 'hs_avatar_filemanager_key',\n 'jobtitle',\n 'hubspot_owner_id',\n 'hs_object_id',\n 'createdate',\n 'hs_lastmodifieddate',\n ];\n }\n\n /**\n * @inheritdoc\n */\n private function importContact($crmData, array $accountMappings = []): ?Contact\n {\n $crmProviderId = $crmData['id'] ?? null;\n\n $this->logger->info('[HubSpot] importContact', [\n 'crm_provider_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n if (empty($crmData['properties'])) {\n $this->logger->info('[HubSpot] importContact failed: empty properties', [\n 'crm_provider_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n return null;\n }\n\n $properties = $crmData['properties'];\n $crmId = (string) $crmData['id'];\n\n $accountId = $this->resolveContactAccount($properties, $accountMappings);\n $data = $this->buildContactData($crmId, $properties, $accountId);\n\n return $this->crmEntityRepository->importContact($this->config, $data);\n }\n\n private function resolveContactAccount(array $properties, array $accountMappings): ?int\n {\n if (empty($properties['associatedcompanyid'])) {\n return null;\n }\n\n $companyId = (string) $properties['associatedcompanyid'];\n\n if (! empty($accountMappings)) {\n return $accountMappings[$companyId] ?? null;\n }\n\n return $this->crmEntityRepository->findAccountByExternalId(\n $this->team->getCrmConfiguration(),\n $companyId\n )?->getId() ?? $this->syncAccount($companyId)?->getId();\n }\n\n private function buildContactData(string $crmId, array $properties, ?int $accountId): array\n {\n $countryCode = $this->buildContactCountry($properties);\n $name = $this->buildContactName($properties);\n $photoPath = $this->teamService->generateAvatar(\n $crmId,\n empty($name) ? ($properties['email'] ?? 'N/A') : $name,\n );\n $parsedNumber = $this->buildContactPhone($countryCode, $properties);\n $mobileNumber = $this->buildContactMobilePhone($countryCode, $properties);\n\n $ownerId = $properties['hubspot_owner_id'] ?? null;\n $profile = $ownerId !== null\n ? $this->crmEntityRepository->findProfileByExternalId($this->config, (string) $ownerId)\n : null;\n\n $ext = (isset($parsedNumber['ext']) && is_string($parsedNumber['ext']) && strlen($parsedNumber['ext']) <= 10)\n ? $parsedNumber['ext']\n : null;\n\n $title = isset($properties['jobtitle']) ? mb_strimwidth($properties['jobtitle'], 0, 128) : null;\n $email = isset($properties['email']) ? mb_strimwidth($properties['email'], 0, 191) : null;\n $remotelyCreatedAt = ! empty($properties['createdate']) ? Carbon::parse($properties['createdate']) : null;\n\n return [\n 'crm_provider_id' => $crmId,\n 'team_id' => $this->team->getId(),\n 'account_id' => $accountId,\n 'user_id' => $profile?->getUserId(),\n 'owner_id' => $ownerId,\n 'name' => $name,\n 'title' => $title,\n 'email' => $email,\n 'country_code' => $countryCode,\n 'phone' => $parsedNumber['phone'] ?? null,\n 'mobile_phone' => $mobileNumber ?? null,\n 'ext' => $ext,\n 'photo_path' => $photoPath,\n 'remotely_created_at' => $remotelyCreatedAt,\n ];\n }\n\n /**\n * @param $properties\n */\n private function buildContactName($properties): string\n {\n if (is_array($properties)) {\n return $this->buildContactNameFromArray($properties);\n }\n\n return $this->buildContactNameFromObject($properties);\n }\n\n private function buildContactNameFromArray(array $properties): string\n {\n if (! empty($properties['name'])) {\n return mb_strimwidth($properties['name'], 0, 100);\n }\n\n $name = '';\n if (! empty($properties['firstname'])) {\n $name = $properties['firstname'] . ' ';\n }\n\n if (! empty($properties['lastname'])) {\n $name .= $properties['lastname'];\n }\n\n if ($name === '' && ! empty($properties['email'])) {\n $name = $properties['email'];\n }\n\n return mb_strimwidth($name, 0, 100);\n }\n\n private function buildContactNameFromObject($properties): string\n {\n $name = '';\n if (isset($properties->firstname)) {\n $name = $properties->firstname->value . ' ';\n }\n if (isset($properties->lastname)) {\n $name .= $properties->lastname->value;\n }\n if ($name === '' && isset($properties->email)) {\n $name = $properties->email->value;\n }\n\n return mb_strimwidth($name, 0, 100);\n }\n\n /**\n * @param $properties\n */\n private function buildContactPhone(?string $countryCode, $properties): ?array\n {\n if (is_array($properties) && empty($properties['phone']) === false) {\n $number = mb_strimwidth($properties['phone'], 0, 25);\n\n return parsePhoneNumber($countryCode, $number);\n } elseif (isset($properties->phone)) {\n $number = mb_strimwidth($properties->phone->value, 0, 25);\n\n return parsePhoneNumber($countryCode, $number);\n }\n\n return [];\n }\n\n /**\n * @param $properties\n */\n private function buildContactMobilePhone(?string $countryCode, $properties): ?string\n {\n return isset($properties['mobilephone'])\n ? Str::limit(phone_e164($countryCode, $properties['mobilephone']), 25, '')\n : null;\n }\n\n /**\n * @param $properties\n * @param $account\n */\n private function buildContactCountry($properties): ?string\n {\n if (is_array($properties) && empty($properties['country']) === false) {\n return $this->convertCountryNameToCode($properties['country']);\n }\n\n if (isset($properties->country)) {\n return $this->convertCountryNameToCode($properties->country->value);\n }\n\n return null;\n }\n\n /**\n * HubSpot doesn't have leads, so this method does nothing.\n *\n * @param Carbon $since\n * @param Carbon|null $to\n * @param string|null $crmProfileId\n *\n * @return int\n */\n public function syncLeads(Carbon $since, ?Carbon $to = null, ?string $crmProfileId = null): int\n {\n // Mark unused parameters to avoid code smell warnings\n unset($since, $to, $crmProfileId);\n\n return 0;\n }\n\n /**\n * HubSpot doesn't have leads.\n *\n * @param string $crmId\n *\n * @inheritdoc\n */\n public function syncLead(string $crmId): ?Lead\n {\n // Mark unused parameter to avoid code smell warnings\n unset($crmId);\n\n return null;\n }\n\n /**\n * Sync accounts (companies) modified since a given date (manual sync mode).\n *\n * This method fetches companies from HubSpot API based on modification date and\n * imports them one by one. It is used for:\n * - Manual sync commands (e.g., crm:sync-account with --from parameter)\n * - Initial sync for new teams\n * - Backfill operations\n *\n * For regular sync webhook batchSyncCompanies is used:\n *\n * @param Carbon $since Fetch companies modified after this date\n * @param Carbon|null $to Optional end date for modification range\n *\n * @return int Number of companies successfully synced\n */\n public function syncAccounts(Carbon $since, ?Carbon $to = null): int\n {\n $syncCount = 0;\n\n try {\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $fields = $this->getCompanyFields();\n $payload = $this->payloadBuilder->getRecentlyUpdatedSearchPayload($since, $to, $fields);\n\n foreach ($this->client->getPaginatedDataGenerator($payload, 'companies') as $hsAccount) {\n $this->importAccount($hsAccount);\n $syncCount++;\n }\n } catch (Exception $exception) {\n $this->logger->error('[' . $this->getDisplayName() . '] Sync accounts failed', [\n 'teamId' => $this->team->getUuid(),\n 'reason' => $exception->getMessage(),\n ]);\n }\n\n return $syncCount;\n }\n\n /**\n * @inheritdoc\n */\n public function syncAccount(string $crmId): ?Account\n {\n try {\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $fields = $this->getCompanyFields();\n $hsAccount = $this->client->getAccountById($crmId, $fields);\n } catch (\\HubSpot\\Client\\Crm\\Companies\\ApiException $e) {\n $this->logger->info('[' . $this->getDisplayName() . '] Account fetch failed', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n return null;\n } catch (CrmException $e) {\n $this->logger->info('[' . $this->getDisplayName() . '] Account not found', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n return null;\n }\n\n return $this->importAccount($hsAccount);\n }\n\n /**\n * Process webhook-collected contact batches.\n *\n * Drains Redis sets containing contact CRM IDs collected from webhook events\n * and dispatches ImportContactBatch jobs for batch processing.\n *\n * @return int Number of contact IDs dispatched to jobs\n */\n public function batchSyncContacts(): int\n {\n $configId = $this->team->getCrmConfiguration()->getId();\n\n return $this->batchProcessor->processBatchesForObjectType(\n WebhookSyncBatchProcessor::OBJECT_TYPE_CONTACT,\n $configId\n );\n }\n\n public function importContactBatchByIds(array $crmIds): array\n {\n $result = [\n 'success_count' => 0,\n 'failed_ids' => [],\n 'errors' => [],\n ];\n\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $batchStart = microtime(true);\n $slowContacts = [];\n\n $fetchStart = microtime(true);\n $allContacts = $this->fetchContactsByIdsInChunks($crmIds);\n $fetchMs = (int) round((microtime(true) - $fetchStart) * 1000);\n\n $fetchedIds = array_map('strval', array_column($allContacts, 'id'));\n $notFoundIds = array_values(array_diff(array_map('strval', $crmIds), $fetchedIds));\n\n if (! empty($notFoundIds)) {\n $this->logger->info('[' . $this->getDisplayName() . '] Contact CRM IDs not found in HubSpot', [\n 'teamId' => $this->team->getId(),\n 'notFoundCount' => \\count($notFoundIds),\n 'notFoundIds' => $notFoundIds,\n 'requestedCount' => \\count($crmIds),\n 'fetchedCount' => \\count($allContacts),\n ]);\n }\n\n if (empty($allContacts)) {\n return $result;\n }\n\n $prepareStart = microtime(true);\n $accountMappings = $this->prepareAccountMappingsForContacts($allContacts);\n $prepareAccountsMs = (int) round((microtime(true) - $prepareStart) * 1000);\n\n $loopStart = microtime(true);\n foreach ($allContacts as $contactData) {\n $contactStart = microtime(true);\n\n try {\n $contact = $this->importContact($contactData, $accountMappings);\n if ($contact !== null) {\n $result['success_count']++;\n }\n } catch (\\Throwable $e) {\n $crmId = $contactData['id'] ?? 'unknown';\n $result['failed_ids'][] = $crmId;\n $result['errors'][$crmId] = $e->getMessage();\n\n $this->logger->warning('[' . $this->getDisplayName() . '] Failed to import contact', [\n 'teamId' => $this->team->getId(),\n 'crmId' => $crmId,\n 'error' => $e->getMessage(),\n ]);\n }\n\n $contactMs = (int) round((microtime(true) - $contactStart) * 1000);\n if ($contactMs > 1000) {\n $slowContacts[] = ['crmId' => $contactData['id'] ?? 'unknown', 'ms' => $contactMs];\n }\n }\n $loopMs = (int) round((microtime(true) - $loopStart) * 1000);\n $totalMs = (int) round((microtime(true) - $batchStart) * 1000);\n\n $this->logger->info('[' . $this->getDisplayName() . '] importContactBatch timing', [\n 'teamId' => $this->team->getId(),\n 'contact_count' => \\count($allContacts),\n 'requested_count' => \\count($crmIds),\n 'not_found_count' => \\count($notFoundIds),\n 'total_ms' => $totalMs,\n 'fetch_api_ms' => $fetchMs,\n 'prepare_accounts_ms' => $prepareAccountsMs,\n 'contacts_loop_ms' => $loopMs,\n 'avg_contact_ms' => ! empty($allContacts) ? (int) round($loopMs / \\count($allContacts)) : 0,\n 'slow_contacts_count' => \\count($slowContacts),\n 'slow_contacts' => array_slice($slowContacts, 0, 10),\n ]);\n\n return $result;\n }\n\n private function fetchContactsByIdsInChunks(array $crmIds): array\n {\n $fields = $this->getContactFields();\n $allContacts = [];\n\n foreach (array_chunk($crmIds, self::BATCH_SIZE) as $chunk) {\n try {\n $contacts = $this->client->getContactsByIds($chunk, $fields);\n foreach ($contacts as $contactData) {\n $allContacts[] = $contactData;\n }\n } catch (\\Throwable $e) {\n // @TODO what will happen if this exception is thrown\n $this->logger->warning('[' . $this->getDisplayName() . '] Batch contact fetch failed', [\n 'chunk_size' => \\count($chunk),\n 'error' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n }\n\n return $allContacts;\n }\n\n private function prepareAccountMappingsForContacts(array $contacts): array\n {\n $companyIds = [];\n foreach ($contacts as $contact) {\n $companyId = $contact['properties']['associatedcompanyid'] ?? null;\n if ($companyId !== null && $companyId !== '') {\n $companyIds[] = (string) $companyId;\n }\n }\n\n $companyIds = array_unique($companyIds);\n\n if (empty($companyIds)) {\n return [];\n }\n\n $mappings = $this->crmEntityRepository\n ->getExistingAccountIdsMap($this->config, $companyIds);\n\n $missingCompanyIds = array_diff($companyIds, array_keys($mappings));\n\n if (empty($missingCompanyIds)) {\n return $mappings;\n }\n\n $this->logger->info('[' . $this->getDisplayName() . '] Batch syncing missing accounts for contacts', [\n 'teamId' => $this->team->getId(),\n 'total_companies' => \\count($companyIds),\n 'existing_companies' => \\count($mappings),\n 'missing_companies' => \\count($missingCompanyIds),\n ]);\n\n try {\n $syncedAccounts = $this->batchSyncAccountsForContacts($missingCompanyIds);\n $mappings = array_merge($mappings, $syncedAccounts);\n } catch (\\Throwable $e) {\n $this->logger->error('[' . $this->getDisplayName() . '] Failed to batch sync missing accounts', [\n 'teamId' => $this->team->getId(),\n 'missingCompanyIds' => $missingCompanyIds,\n 'missingCount' => count($missingCompanyIds),\n 'error' => $e->getMessage(),\n ]);\n }\n\n return $mappings;\n }\n\n private function batchSyncAccountsForContacts(array $companyIds): array\n {\n $syncedAccounts = [];\n $fields = $this->getCompanyFields();\n\n foreach (array_chunk($companyIds, self::BATCH_SIZE) as $chunk) {\n try {\n $companies = $this->client->getCompaniesByIds($chunk, $fields);\n\n foreach ($companies as $companyData) {\n try {\n $account = $this->importAccount($companyData);\n if ($account) {\n $syncedAccounts[$account->getCrmProviderId()] = $account->getId();\n }\n } catch (\\Throwable $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Failed to import account in batch', [\n 'teamId' => $this->team->getId(),\n 'companyId' => $companyData['id'] ?? 'unknown',\n 'error' => $e->getMessage(),\n ]);\n }\n }\n } catch (\\Throwable $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Batch company fetch failed', [\n 'teamId' => $this->team->getId(),\n 'chunk_size' => \\count($chunk),\n 'error' => $e->getMessage(),\n ]);\n }\n }\n\n return $syncedAccounts;\n }\n\n /**\n * Process webhook-collected company batches.\n *\n * Drains Redis sets containing company CRM IDs collected from webhook events\n * and dispatches ImportAccountBatch jobs for batch processing.\n *\n * @return int Number of company IDs dispatched to jobs\n */\n public function batchSyncCompanies(): int\n {\n $configId = $this->team->getCrmConfiguration()->getId();\n\n return $this->batchProcessor->processBatchesForObjectType(\n WebhookSyncBatchProcessor::OBJECT_TYPE_COMPANY,\n $configId\n );\n }\n\n public function importAccountBatchByIds(array $crmIds): array\n {\n $result = [\n 'success_count' => 0,\n 'failed_ids' => [],\n 'errors' => [],\n ];\n\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $batchStart = microtime(true);\n $slowAccounts = [];\n\n $fields = $this->getCompanyFields();\n $allCompanies = [];\n\n $fetchStart = microtime(true);\n foreach (array_chunk($crmIds, self::BATCH_SIZE) as $chunk) {\n try {\n $companies = $this->client->getCompaniesByIds($chunk, $fields);\n foreach ($companies as $companyData) {\n $allCompanies[] = $companyData;\n }\n } catch (\\Throwable $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Batch company fetch failed', [\n 'chunk_size' => \\count($chunk),\n 'error' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n }\n $fetchMs = (int) round((microtime(true) - $fetchStart) * 1000);\n\n $fetchedIds = array_map('strval', array_column($allCompanies, 'id'));\n $notFoundIds = array_values(array_diff(array_map('strval', $crmIds), $fetchedIds));\n\n if (! empty($notFoundIds)) {\n $this->logger->info('[' . $this->getDisplayName() . '] Company CRM IDs not found in HubSpot', [\n 'teamId' => $this->team->getId(),\n 'notFoundCount' => \\count($notFoundIds),\n 'notFoundIds' => $notFoundIds,\n 'requestedCount' => \\count($crmIds),\n 'fetchedCount' => \\count($allCompanies),\n ]);\n }\n\n $loopStart = microtime(true);\n foreach ($allCompanies as $companyData) {\n $accountStart = microtime(true);\n\n try {\n $account = $this->importAccount($companyData);\n if ($account !== null) {\n $result['success_count']++;\n }\n } catch (\\Throwable $e) {\n $crmId = $companyData['id'] ?? $companyData['properties']['hs_object_id'] ?? 'unknown';\n $result['failed_ids'][] = $crmId;\n $result['errors'][$crmId] = $e->getMessage();\n\n $this->logger->warning('[' . $this->getDisplayName() . '] Failed to import account', [\n 'crmId' => $crmId,\n 'error' => $e->getMessage(),\n ]);\n }\n\n $accountMs = (int) round((microtime(true) - $accountStart) * 1000);\n if ($accountMs > 1000) {\n $crmId = $companyData['id'] ?? $companyData['properties']['hs_object_id'] ?? 'unknown';\n $slowAccounts[] = ['crmId' => $crmId, 'ms' => $accountMs];\n }\n }\n $loopMs = (int) round((microtime(true) - $loopStart) * 1000);\n $totalMs = (int) round((microtime(true) - $batchStart) * 1000);\n\n $this->logger->info('[' . $this->getDisplayName() . '] importAccountBatch timing', [\n 'teamId' => $this->team->getId(),\n 'account_count' => \\count($allCompanies),\n 'requested_count' => \\count($crmIds),\n 'not_found_count' => \\count($notFoundIds),\n 'total_ms' => $totalMs,\n 'fetch_api_ms' => $fetchMs,\n 'accounts_loop_ms' => $loopMs,\n 'avg_account_ms' => ! empty($allCompanies) ? (int) round($loopMs / \\count($allCompanies)) : 0,\n 'slow_accounts_count' => \\count($slowAccounts),\n 'slow_accounts' => array_slice($slowAccounts, 0, 10),\n ]);\n\n return $result;\n }\n\n private function getCompanyFields(): array\n {\n return [\n 'country',\n 'name',\n 'phone',\n 'domain',\n 'hs_avatar_filemanager_key',\n 'industry',\n 'hubspot_owner_id',\n 'hs_object_id',\n 'createdate',\n 'hs_lastmodifieddate',\n ];\n }\n\n private function importAccount($crmData): ?Account\n {\n $crmProviderId = $crmData['id'] ?? $crmData['properties']['hs_object_id'] ?? null;\n\n $this->logger->info('[HubSpot] importAccount', [\n 'crm_provider_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n if (empty($crmData['properties'])) {\n $this->logger->info('[HubSpot] importAccount failed: empty properties', [\n 'crm_provider_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n return null;\n }\n\n $properties = $crmData['properties'];\n $crmId = (string) $properties['hs_object_id'];\n\n $countryCode = isset($properties['country']) ? $this->convertCountryNameToCode($properties['country']) : null;\n\n if (isset($properties['phone'])) {\n // Trim to our width and attempt to parse it.\n $number = mb_strimwidth($properties['phone'], 0, 25);\n $parsedNumber = parsePhoneNumber($countryCode, $number);\n } else {\n $parsedNumber = [];\n }\n\n $name = '[unknown]';\n if (isset($properties['name'])) {\n $name = $properties['name'];\n }\n\n $photoPath = $this->prospectPhotoPathService->getOrGeneratePhotoPath(\n $this->config,\n $crmId,\n Account::class,\n $crmId,\n $name\n );\n\n $industry = null;\n if (isset($properties['industry'])) {\n $industry = mb_strimwidth($properties['industry'], 0, 40);\n }\n\n $ownerId = $profile = null;\n if (isset($properties['hubspot_owner_id'])) {\n $ownerId = $properties['hubspot_owner_id'];\n $profile = $this->crmEntityRepository->findProfileByExternalId($this->config, (string) $ownerId);\n }\n\n $domain = null;\n if (isset($properties['domain'])) {\n $domain = StringUtil::resolveDomain($properties['domain']);\n }\n\n $remotelyCreatedAt = null;\n if (isset($properties['createdate']) && ! empty($properties['createdate'])) {\n $remotelyCreatedAt = Carbon::parse($properties['createdate']);\n }\n\n $data = [\n 'crm_provider_id' => $crmId,\n 'team_id' => $this->team->id,\n 'user_id' => $profile ? $profile->user_id : null,\n 'owner_id' => $ownerId,\n 'name' => mb_strimwidth($name, 0, 191),\n 'photo_path' => $photoPath,\n 'industry' => $industry,\n 'domain' => $domain !== null\n ? substr($domain, 0, 191)\n : null,\n 'phone' => $parsedNumber['phone'] ?? null,\n 'ext' => $parsedNumber['ext'] ?? null,\n 'country_code' => $countryCode,\n 'remotely_created_at' => $remotelyCreatedAt,\n ];\n\n return $this->crmEntityRepository->importAccount($this->config, $data);\n }\n\n public function deleteContact(string $crmProviderId): bool\n {\n try {\n $contact = $this->crmEntityRepository->findContactByExternalId($this->config, $crmProviderId);\n\n if (! $contact) {\n $this->logger->info('[HubSpot] Contact not found for deletion', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n // return success because we do not know which instance is the target\n return true;\n }\n\n $id = $contact->getId();\n\n $this->logger->info('[HubSpot] Deleting contact via webhook', [\n 'contact_id' => $id,\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n $contact->delete();\n DeleteContactJob::dispatch($id)->afterCommit();\n\n return true;\n } catch (Exception $e) {\n $this->logger->error('[HubSpot] Failed to delete contact via webhook', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n return false;\n }\n }\n\n public function deleteAccount(string $crmProviderId): bool\n {\n try {\n $account = $this->crmEntityRepository->findAccountByExternalId($this->config, $crmProviderId);\n\n if (! $account) {\n $this->logger->info('[HubSpot] Account not found for deletion', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n // return success because we do not know which instance is the target\n return true;\n }\n\n $id = $account->getId();\n\n $this->logger->info('[HubSpot] Deleting account via webhook', [\n 'account_id' => $id,\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n $account->delete();\n DeleteAccountJob::dispatch($id)->afterCommit();\n\n return true;\n } catch (Exception $e) {\n $this->logger->error('[HubSpot] Failed to delete account via webhook', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n return false;\n }\n }\n\n public function deleteOpportunity(string $crmProviderId): bool\n {\n try {\n $opportunity = $this->crmEntityRepository->findOpportunityByExternalId($this->config, $crmProviderId);\n\n if (! $opportunity) {\n $this->logger->info('[HubSpot] Opportunity not found for deletion', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n // return success because we do not know which instance is the target\n return true;\n }\n\n $id = $opportunity->getId();\n\n $this->logger->info('[HubSpot] Deleting opportunity via webhook', [\n 'opportunity_id' => $id,\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n $opportunity->delete();\n DeleteOpportunityJob::dispatch($id)->afterCommit();\n\n return true;\n } catch (Exception $e) {\n $this->logger->error('[HubSpot] Failed to delete opportunity via webhook', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n return false;\n }\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits;\n\nuse Carbon\\Carbon;\nuse Exception;\nuse Illuminate\\Support\\Str;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Jobs\\Crm\\Delete\\DeleteAccountJob;\nuse Jiminny\\Jobs\\Crm\\Delete\\DeleteContactJob;\nuse Jiminny\\Jobs\\Crm\\Delete\\DeleteOpportunityJob;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Repositories\\Crm\\CrmEntityRepository;\nuse Jiminny\\Services\\Avatar\\ProspectPhotoPathService;\nuse Jiminny\\Services\\Crm\\Hubspot\\HubspotClientInterface;\nuse Jiminny\\Services\\Crm\\Hubspot\\WebhookSyncBatchProcessor;\nuse Jiminny\\Utils\\StringUtil;\n\ntrait SyncCrmEntitiesTrait\n{\n use OpportunitySyncTrait;\n private const string CDN_URL = 'https://cdn2.hubspot.net/';\n protected CrmEntityRepository $crmEntityRepository;\n protected ProspectPhotoPathService $prospectPhotoPathService;\n\n private function getAssociationDataForCollection(array $collection, string $fromObject, string $toObject): array\n {\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $hsOpportunityIds = array_column($collection, 'id');\n\n return $this->client->getAssociationsData($hsOpportunityIds, $fromObject, $toObject);\n }\n\n private function importAssociationData(array $collection, array $associatedData): array\n {\n $data = [];\n if (! empty($associatedData[$collection['id']])) {\n foreach ($associatedData[$collection['id']] as $id) {\n $data[] = [\n 'id' => $id,\n ];\n }\n }\n\n return ['results' => $data];\n }\n\n /**\n * Sync contacts modified since a given date (manual sync mode).\n *\n * This method fetches contacts from HubSpot API based on modification date and\n * imports them one by one. It is used for:\n * - Manual sync commands (e.g., crm:sync-contact with --from parameter)\n * - Initial sync for new teams\n * - Backfill operations\n *\n * For regular sync webhook batchSyncContacts is used:\n *\n * @param Carbon $since Fetch contacts modified after this date\n * @param Carbon|null $to Optional end date for modification range\n *\n * @return int Number of contacts successfully synced\n */\n public function syncContacts(Carbon $since, ?Carbon $to = null): int\n {\n $syncCount = 0;\n\n try {\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $fields = $this->getContactFields();\n $payload = $this->payloadBuilder->getRecentlyUpdatedSearchPayload($since, $to, $fields);\n\n foreach ($this->client->getPaginatedDataGenerator($payload, 'contacts') as $hsContact) {\n $this->importContact($hsContact);\n $syncCount++;\n }\n } catch (Exception $exception) {\n $this->logger->error('[' . $this->getDisplayName() . '] Sync contacts failed', [\n 'teamId' => $this->team->getUuid(),\n 'reason' => $exception->getMessage(),\n ]);\n }\n\n return $syncCount;\n }\n\n /**\n * @inheritdoc\n */\n public function syncContact(string $crmId): ?Contact\n {\n try {\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $fields = $this->getContactFields();\n $hsContact = $this->client->getContactById($crmId, $fields);\n } catch (\\HubSpot\\Client\\Crm\\Contacts\\ApiException $e) {\n $this->logger->info('[' . $this->getDisplayName() . '] Contact fetch failed', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n return null;\n } catch (CrmException $e) {\n $this->logger->info('[' . $this->getDisplayName() . '] Contact not found', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n return null;\n }\n\n if (empty($hsContact['properties']) || empty($hsContact['id'])) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Contact data incomplete', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'has_properties' => ! empty($hsContact['properties']),\n 'has_id' => ! empty($hsContact['id']),\n ]);\n\n return null;\n }\n\n return $this->importContact($hsContact);\n }\n\n private function getContactFields(): array\n {\n return [\n 'associatedcompanyid',\n 'country',\n 'firstname',\n 'lastname',\n 'phone',\n 'mobilephone',\n 'email',\n 'photo',\n 'hs_avatar_filemanager_key',\n 'jobtitle',\n 'hubspot_owner_id',\n 'hs_object_id',\n 'createdate',\n 'hs_lastmodifieddate',\n ];\n }\n\n /**\n * @inheritdoc\n */\n private function importContact($crmData, array $accountMappings = []): ?Contact\n {\n $crmProviderId = $crmData['id'] ?? null;\n\n $this->logger->info('[HubSpot] importContact', [\n 'crm_provider_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n if (empty($crmData['properties'])) {\n $this->logger->info('[HubSpot] importContact failed: empty properties', [\n 'crm_provider_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n return null;\n }\n\n $properties = $crmData['properties'];\n $crmId = (string) $crmData['id'];\n\n $accountId = $this->resolveContactAccount($properties, $accountMappings);\n $data = $this->buildContactData($crmId, $properties, $accountId);\n\n return $this->crmEntityRepository->importContact($this->config, $data);\n }\n\n private function resolveContactAccount(array $properties, array $accountMappings): ?int\n {\n if (empty($properties['associatedcompanyid'])) {\n return null;\n }\n\n $companyId = (string) $properties['associatedcompanyid'];\n\n if (! empty($accountMappings)) {\n return $accountMappings[$companyId] ?? null;\n }\n\n return $this->crmEntityRepository->findAccountByExternalId(\n $this->team->getCrmConfiguration(),\n $companyId\n )?->getId() ?? $this->syncAccount($companyId)?->getId();\n }\n\n private function buildContactData(string $crmId, array $properties, ?int $accountId): array\n {\n $countryCode = $this->buildContactCountry($properties);\n $name = $this->buildContactName($properties);\n $photoPath = $this->teamService->generateAvatar(\n $crmId,\n empty($name) ? ($properties['email'] ?? 'N/A') : $name,\n );\n $parsedNumber = $this->buildContactPhone($countryCode, $properties);\n $mobileNumber = $this->buildContactMobilePhone($countryCode, $properties);\n\n $ownerId = $properties['hubspot_owner_id'] ?? null;\n $profile = $ownerId !== null\n ? $this->crmEntityRepository->findProfileByExternalId($this->config, (string) $ownerId)\n : null;\n\n $ext = (isset($parsedNumber['ext']) && is_string($parsedNumber['ext']) && strlen($parsedNumber['ext']) <= 10)\n ? $parsedNumber['ext']\n : null;\n\n $title = isset($properties['jobtitle']) ? mb_strimwidth($properties['jobtitle'], 0, 128) : null;\n $email = isset($properties['email']) ? mb_strimwidth($properties['email'], 0, 191) : null;\n $remotelyCreatedAt = ! empty($properties['createdate']) ? Carbon::parse($properties['createdate']) : null;\n\n return [\n 'crm_provider_id' => $crmId,\n 'team_id' => $this->team->getId(),\n 'account_id' => $accountId,\n 'user_id' => $profile?->getUserId(),\n 'owner_id' => $ownerId,\n 'name' => $name,\n 'title' => $title,\n 'email' => $email,\n 'country_code' => $countryCode,\n 'phone' => $parsedNumber['phone'] ?? null,\n 'mobile_phone' => $mobileNumber ?? null,\n 'ext' => $ext,\n 'photo_path' => $photoPath,\n 'remotely_created_at' => $remotelyCreatedAt,\n ];\n }\n\n /**\n * @param $properties\n */\n private function buildContactName($properties): string\n {\n if (is_array($properties)) {\n return $this->buildContactNameFromArray($properties);\n }\n\n return $this->buildContactNameFromObject($properties);\n }\n\n private function buildContactNameFromArray(array $properties): string\n {\n if (! empty($properties['name'])) {\n return mb_strimwidth($properties['name'], 0, 100);\n }\n\n $name = '';\n if (! empty($properties['firstname'])) {\n $name = $properties['firstname'] . ' ';\n }\n\n if (! empty($properties['lastname'])) {\n $name .= $properties['lastname'];\n }\n\n if ($name === '' && ! empty($properties['email'])) {\n $name = $properties['email'];\n }\n\n return mb_strimwidth($name, 0, 100);\n }\n\n private function buildContactNameFromObject($properties): string\n {\n $name = '';\n if (isset($properties->firstname)) {\n $name = $properties->firstname->value . ' ';\n }\n if (isset($properties->lastname)) {\n $name .= $properties->lastname->value;\n }\n if ($name === '' && isset($properties->email)) {\n $name = $properties->email->value;\n }\n\n return mb_strimwidth($name, 0, 100);\n }\n\n /**\n * @param $properties\n */\n private function buildContactPhone(?string $countryCode, $properties): ?array\n {\n if (is_array($properties) && empty($properties['phone']) === false) {\n $number = mb_strimwidth($properties['phone'], 0, 25);\n\n return parsePhoneNumber($countryCode, $number);\n } elseif (isset($properties->phone)) {\n $number = mb_strimwidth($properties->phone->value, 0, 25);\n\n return parsePhoneNumber($countryCode, $number);\n }\n\n return [];\n }\n\n /**\n * @param $properties\n */\n private function buildContactMobilePhone(?string $countryCode, $properties): ?string\n {\n return isset($properties['mobilephone'])\n ? Str::limit(phone_e164($countryCode, $properties['mobilephone']), 25, '')\n : null;\n }\n\n /**\n * @param $properties\n * @param $account\n */\n private function buildContactCountry($properties): ?string\n {\n if (is_array($properties) && empty($properties['country']) === false) {\n return $this->convertCountryNameToCode($properties['country']);\n }\n\n if (isset($properties->country)) {\n return $this->convertCountryNameToCode($properties->country->value);\n }\n\n return null;\n }\n\n /**\n * HubSpot doesn't have leads, so this method does nothing.\n *\n * @param Carbon $since\n * @param Carbon|null $to\n * @param string|null $crmProfileId\n *\n * @return int\n */\n public function syncLeads(Carbon $since, ?Carbon $to = null, ?string $crmProfileId = null): int\n {\n // Mark unused parameters to avoid code smell warnings\n unset($since, $to, $crmProfileId);\n\n return 0;\n }\n\n /**\n * HubSpot doesn't have leads.\n *\n * @param string $crmId\n *\n * @inheritdoc\n */\n public function syncLead(string $crmId): ?Lead\n {\n // Mark unused parameter to avoid code smell warnings\n unset($crmId);\n\n return null;\n }\n\n /**\n * Sync accounts (companies) modified since a given date (manual sync mode).\n *\n * This method fetches companies from HubSpot API based on modification date and\n * imports them one by one. It is used for:\n * - Manual sync commands (e.g., crm:sync-account with --from parameter)\n * - Initial sync for new teams\n * - Backfill operations\n *\n * For regular sync webhook batchSyncCompanies is used:\n *\n * @param Carbon $since Fetch companies modified after this date\n * @param Carbon|null $to Optional end date for modification range\n *\n * @return int Number of companies successfully synced\n */\n public function syncAccounts(Carbon $since, ?Carbon $to = null): int\n {\n $syncCount = 0;\n\n try {\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $fields = $this->getCompanyFields();\n $payload = $this->payloadBuilder->getRecentlyUpdatedSearchPayload($since, $to, $fields);\n\n foreach ($this->client->getPaginatedDataGenerator($payload, 'companies') as $hsAccount) {\n $this->importAccount($hsAccount);\n $syncCount++;\n }\n } catch (Exception $exception) {\n $this->logger->error('[' . $this->getDisplayName() . '] Sync accounts failed', [\n 'teamId' => $this->team->getUuid(),\n 'reason' => $exception->getMessage(),\n ]);\n }\n\n return $syncCount;\n }\n\n /**\n * @inheritdoc\n */\n public function syncAccount(string $crmId): ?Account\n {\n try {\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $fields = $this->getCompanyFields();\n $hsAccount = $this->client->getAccountById($crmId, $fields);\n } catch (\\HubSpot\\Client\\Crm\\Companies\\ApiException $e) {\n $this->logger->info('[' . $this->getDisplayName() . '] Account fetch failed', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n return null;\n } catch (CrmException $e) {\n $this->logger->info('[' . $this->getDisplayName() . '] Account not found', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n return null;\n }\n\n return $this->importAccount($hsAccount);\n }\n\n /**\n * Process webhook-collected contact batches.\n *\n * Drains Redis sets containing contact CRM IDs collected from webhook events\n * and dispatches ImportContactBatch jobs for batch processing.\n *\n * @return int Number of contact IDs dispatched to jobs\n */\n public function batchSyncContacts(): int\n {\n $configId = $this->team->getCrmConfiguration()->getId();\n\n return $this->batchProcessor->processBatchesForObjectType(\n WebhookSyncBatchProcessor::OBJECT_TYPE_CONTACT,\n $configId\n );\n }\n\n public function importContactBatchByIds(array $crmIds): array\n {\n $result = [\n 'success_count' => 0,\n 'failed_ids' => [],\n 'errors' => [],\n ];\n\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $batchStart = microtime(true);\n $slowContacts = [];\n\n $fetchStart = microtime(true);\n $allContacts = $this->fetchContactsByIdsInChunks($crmIds);\n $fetchMs = (int) round((microtime(true) - $fetchStart) * 1000);\n\n $fetchedIds = array_map('strval', array_column($allContacts, 'id'));\n $notFoundIds = array_values(array_diff(array_map('strval', $crmIds), $fetchedIds));\n\n if (! empty($notFoundIds)) {\n $this->logger->info('[' . $this->getDisplayName() . '] Contact CRM IDs not found in HubSpot', [\n 'teamId' => $this->team->getId(),\n 'notFoundCount' => \\count($notFoundIds),\n 'notFoundIds' => $notFoundIds,\n 'requestedCount' => \\count($crmIds),\n 'fetchedCount' => \\count($allContacts),\n ]);\n }\n\n if (empty($allContacts)) {\n return $result;\n }\n\n $prepareStart = microtime(true);\n $accountMappings = $this->prepareAccountMappingsForContacts($allContacts);\n $prepareAccountsMs = (int) round((microtime(true) - $prepareStart) * 1000);\n\n $loopStart = microtime(true);\n foreach ($allContacts as $contactData) {\n $contactStart = microtime(true);\n\n try {\n $contact = $this->importContact($contactData, $accountMappings);\n if ($contact !== null) {\n $result['success_count']++;\n }\n } catch (\\Throwable $e) {\n $crmId = $contactData['id'] ?? 'unknown';\n $result['failed_ids'][] = $crmId;\n $result['errors'][$crmId] = $e->getMessage();\n\n $this->logger->warning('[' . $this->getDisplayName() . '] Failed to import contact', [\n 'teamId' => $this->team->getId(),\n 'crmId' => $crmId,\n 'error' => $e->getMessage(),\n ]);\n }\n\n $contactMs = (int) round((microtime(true) - $contactStart) * 1000);\n if ($contactMs > 1000) {\n $slowContacts[] = ['crmId' => $contactData['id'] ?? 'unknown', 'ms' => $contactMs];\n }\n }\n $loopMs = (int) round((microtime(true) - $loopStart) * 1000);\n $totalMs = (int) round((microtime(true) - $batchStart) * 1000);\n\n $this->logger->info('[' . $this->getDisplayName() . '] importContactBatch timing', [\n 'teamId' => $this->team->getId(),\n 'contact_count' => \\count($allContacts),\n 'requested_count' => \\count($crmIds),\n 'not_found_count' => \\count($notFoundIds),\n 'total_ms' => $totalMs,\n 'fetch_api_ms' => $fetchMs,\n 'prepare_accounts_ms' => $prepareAccountsMs,\n 'contacts_loop_ms' => $loopMs,\n 'avg_contact_ms' => ! empty($allContacts) ? (int) round($loopMs / \\count($allContacts)) : 0,\n 'slow_contacts_count' => \\count($slowContacts),\n 'slow_contacts' => array_slice($slowContacts, 0, 10),\n ]);\n\n return $result;\n }\n\n private function fetchContactsByIdsInChunks(array $crmIds): array\n {\n $fields = $this->getContactFields();\n $allContacts = [];\n\n foreach (array_chunk($crmIds, self::BATCH_SIZE) as $chunk) {\n try {\n $contacts = $this->client->getContactsByIds($chunk, $fields);\n foreach ($contacts as $contactData) {\n $allContacts[] = $contactData;\n }\n } catch (\\Throwable $e) {\n // @TODO what will happen if this exception is thrown\n $this->logger->warning('[' . $this->getDisplayName() . '] Batch contact fetch failed', [\n 'chunk_size' => \\count($chunk),\n 'error' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n }\n\n return $allContacts;\n }\n\n private function prepareAccountMappingsForContacts(array $contacts): array\n {\n $companyIds = [];\n foreach ($contacts as $contact) {\n $companyId = $contact['properties']['associatedcompanyid'] ?? null;\n if ($companyId !== null && $companyId !== '') {\n $companyIds[] = (string) $companyId;\n }\n }\n\n $companyIds = array_unique($companyIds);\n\n if (empty($companyIds)) {\n return [];\n }\n\n $mappings = $this->crmEntityRepository\n ->getExistingAccountIdsMap($this->config, $companyIds);\n\n $missingCompanyIds = array_diff($companyIds, array_keys($mappings));\n\n if (empty($missingCompanyIds)) {\n return $mappings;\n }\n\n $this->logger->info('[' . $this->getDisplayName() . '] Batch syncing missing accounts for contacts', [\n 'teamId' => $this->team->getId(),\n 'total_companies' => \\count($companyIds),\n 'existing_companies' => \\count($mappings),\n 'missing_companies' => \\count($missingCompanyIds),\n ]);\n\n try {\n $syncedAccounts = $this->batchSyncAccountsForContacts($missingCompanyIds);\n $mappings = array_merge($mappings, $syncedAccounts);\n } catch (\\Throwable $e) {\n $this->logger->error('[' . $this->getDisplayName() . '] Failed to batch sync missing accounts', [\n 'teamId' => $this->team->getId(),\n 'missingCompanyIds' => $missingCompanyIds,\n 'missingCount' => count($missingCompanyIds),\n 'error' => $e->getMessage(),\n ]);\n }\n\n return $mappings;\n }\n\n private function batchSyncAccountsForContacts(array $companyIds): array\n {\n $syncedAccounts = [];\n $fields = $this->getCompanyFields();\n\n foreach (array_chunk($companyIds, self::BATCH_SIZE) as $chunk) {\n try {\n $companies = $this->client->getCompaniesByIds($chunk, $fields);\n\n foreach ($companies as $companyData) {\n try {\n $account = $this->importAccount($companyData);\n if ($account) {\n $syncedAccounts[$account->getCrmProviderId()] = $account->getId();\n }\n } catch (\\Throwable $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Failed to import account in batch', [\n 'teamId' => $this->team->getId(),\n 'companyId' => $companyData['id'] ?? 'unknown',\n 'error' => $e->getMessage(),\n ]);\n }\n }\n } catch (\\Throwable $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Batch company fetch failed', [\n 'teamId' => $this->team->getId(),\n 'chunk_size' => \\count($chunk),\n 'error' => $e->getMessage(),\n ]);\n }\n }\n\n return $syncedAccounts;\n }\n\n /**\n * Process webhook-collected company batches.\n *\n * Drains Redis sets containing company CRM IDs collected from webhook events\n * and dispatches ImportAccountBatch jobs for batch processing.\n *\n * @return int Number of company IDs dispatched to jobs\n */\n public function batchSyncCompanies(): int\n {\n $configId = $this->team->getCrmConfiguration()->getId();\n\n return $this->batchProcessor->processBatchesForObjectType(\n WebhookSyncBatchProcessor::OBJECT_TYPE_COMPANY,\n $configId\n );\n }\n\n public function importAccountBatchByIds(array $crmIds): array\n {\n $result = [\n 'success_count' => 0,\n 'failed_ids' => [],\n 'errors' => [],\n ];\n\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $batchStart = microtime(true);\n $slowAccounts = [];\n\n $fields = $this->getCompanyFields();\n $allCompanies = [];\n\n $fetchStart = microtime(true);\n foreach (array_chunk($crmIds, self::BATCH_SIZE) as $chunk) {\n try {\n $companies = $this->client->getCompaniesByIds($chunk, $fields);\n foreach ($companies as $companyData) {\n $allCompanies[] = $companyData;\n }\n } catch (\\Throwable $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Batch company fetch failed', [\n 'chunk_size' => \\count($chunk),\n 'error' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n }\n $fetchMs = (int) round((microtime(true) - $fetchStart) * 1000);\n\n $fetchedIds = array_map('strval', array_column($allCompanies, 'id'));\n $notFoundIds = array_values(array_diff(array_map('strval', $crmIds), $fetchedIds));\n\n if (! empty($notFoundIds)) {\n $this->logger->info('[' . $this->getDisplayName() . '] Company CRM IDs not found in HubSpot', [\n 'teamId' => $this->team->getId(),\n 'notFoundCount' => \\count($notFoundIds),\n 'notFoundIds' => $notFoundIds,\n 'requestedCount' => \\count($crmIds),\n 'fetchedCount' => \\count($allCompanies),\n ]);\n }\n\n $loopStart = microtime(true);\n foreach ($allCompanies as $companyData) {\n $accountStart = microtime(true);\n\n try {\n $account = $this->importAccount($companyData);\n if ($account !== null) {\n $result['success_count']++;\n }\n } catch (\\Throwable $e) {\n $crmId = $companyData['id'] ?? $companyData['properties']['hs_object_id'] ?? 'unknown';\n $result['failed_ids'][] = $crmId;\n $result['errors'][$crmId] = $e->getMessage();\n\n $this->logger->warning('[' . $this->getDisplayName() . '] Failed to import account', [\n 'crmId' => $crmId,\n 'error' => $e->getMessage(),\n ]);\n }\n\n $accountMs = (int) round((microtime(true) - $accountStart) * 1000);\n if ($accountMs > 1000) {\n $crmId = $companyData['id'] ?? $companyData['properties']['hs_object_id'] ?? 'unknown';\n $slowAccounts[] = ['crmId' => $crmId, 'ms' => $accountMs];\n }\n }\n $loopMs = (int) round((microtime(true) - $loopStart) * 1000);\n $totalMs = (int) round((microtime(true) - $batchStart) * 1000);\n\n $this->logger->info('[' . $this->getDisplayName() . '] importAccountBatch timing', [\n 'teamId' => $this->team->getId(),\n 'account_count' => \\count($allCompanies),\n 'requested_count' => \\count($crmIds),\n 'not_found_count' => \\count($notFoundIds),\n 'total_ms' => $totalMs,\n 'fetch_api_ms' => $fetchMs,\n 'accounts_loop_ms' => $loopMs,\n 'avg_account_ms' => ! empty($allCompanies) ? (int) round($loopMs / \\count($allCompanies)) : 0,\n 'slow_accounts_count' => \\count($slowAccounts),\n 'slow_accounts' => array_slice($slowAccounts, 0, 10),\n ]);\n\n return $result;\n }\n\n private function getCompanyFields(): array\n {\n return [\n 'country',\n 'name',\n 'phone',\n 'domain',\n 'hs_avatar_filemanager_key',\n 'industry',\n 'hubspot_owner_id',\n 'hs_object_id',\n 'createdate',\n 'hs_lastmodifieddate',\n ];\n }\n\n private function importAccount($crmData): ?Account\n {\n $crmProviderId = $crmData['id'] ?? $crmData['properties']['hs_object_id'] ?? null;\n\n $this->logger->info('[HubSpot] importAccount', [\n 'crm_provider_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n if (empty($crmData['properties'])) {\n $this->logger->info('[HubSpot] importAccount failed: empty properties', [\n 'crm_provider_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n return null;\n }\n\n $properties = $crmData['properties'];\n $crmId = (string) $properties['hs_object_id'];\n\n $countryCode = isset($properties['country']) ? $this->convertCountryNameToCode($properties['country']) : null;\n\n if (isset($properties['phone'])) {\n // Trim to our width and attempt to parse it.\n $number = mb_strimwidth($properties['phone'], 0, 25);\n $parsedNumber = parsePhoneNumber($countryCode, $number);\n } else {\n $parsedNumber = [];\n }\n\n $name = '[unknown]';\n if (isset($properties['name'])) {\n $name = $properties['name'];\n }\n\n $photoPath = $this->prospectPhotoPathService->getOrGeneratePhotoPath(\n $this->config,\n $crmId,\n Account::class,\n $crmId,\n $name\n );\n\n $industry = null;\n if (isset($properties['industry'])) {\n $industry = mb_strimwidth($properties['industry'], 0, 40);\n }\n\n $ownerId = $profile = null;\n if (isset($properties['hubspot_owner_id'])) {\n $ownerId = $properties['hubspot_owner_id'];\n $profile = $this->crmEntityRepository->findProfileByExternalId($this->config, (string) $ownerId);\n }\n\n $domain = null;\n if (isset($properties['domain'])) {\n $domain = StringUtil::resolveDomain($properties['domain']);\n }\n\n $remotelyCreatedAt = null;\n if (isset($properties['createdate']) && ! empty($properties['createdate'])) {\n $remotelyCreatedAt = Carbon::parse($properties['createdate']);\n }\n\n $data = [\n 'crm_provider_id' => $crmId,\n 'team_id' => $this->team->id,\n 'user_id' => $profile ? $profile->user_id : null,\n 'owner_id' => $ownerId,\n 'name' => mb_strimwidth($name, 0, 191),\n 'photo_path' => $photoPath,\n 'industry' => $industry,\n 'domain' => $domain !== null\n ? substr($domain, 0, 191)\n : null,\n 'phone' => $parsedNumber['phone'] ?? null,\n 'ext' => $parsedNumber['ext'] ?? null,\n 'country_code' => $countryCode,\n 'remotely_created_at' => $remotelyCreatedAt,\n ];\n\n return $this->crmEntityRepository->importAccount($this->config, $data);\n }\n\n public function deleteContact(string $crmProviderId): bool\n {\n try {\n $contact = $this->crmEntityRepository->findContactByExternalId($this->config, $crmProviderId);\n\n if (! $contact) {\n $this->logger->info('[HubSpot] Contact not found for deletion', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n // return success because we do not know which instance is the target\n return true;\n }\n\n $id = $contact->getId();\n\n $this->logger->info('[HubSpot] Deleting contact via webhook', [\n 'contact_id' => $id,\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n $contact->delete();\n DeleteContactJob::dispatch($id)->afterCommit();\n\n return true;\n } catch (Exception $e) {\n $this->logger->error('[HubSpot] Failed to delete contact via webhook', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n return false;\n }\n }\n\n public function deleteAccount(string $crmProviderId): bool\n {\n try {\n $account = $this->crmEntityRepository->findAccountByExternalId($this->config, $crmProviderId);\n\n if (! $account) {\n $this->logger->info('[HubSpot] Account not found for deletion', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n // return success because we do not know which instance is the target\n return true;\n }\n\n $id = $account->getId();\n\n $this->logger->info('[HubSpot] Deleting account via webhook', [\n 'account_id' => $id,\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n $account->delete();\n DeleteAccountJob::dispatch($id)->afterCommit();\n\n return true;\n } catch (Exception $e) {\n $this->logger->error('[HubSpot] Failed to delete account via webhook', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n return false;\n }\n }\n\n public function deleteOpportunity(string $crmProviderId): bool\n {\n try {\n $opportunity = $this->crmEntityRepository->findOpportunityByExternalId($this->config, $crmProviderId);\n\n if (! $opportunity) {\n $this->logger->info('[HubSpot] Opportunity not found for deletion', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n // return success because we do not know which instance is the target\n return true;\n }\n\n $id = $opportunity->getId();\n\n $this->logger->info('[HubSpot] Deleting opportunity via webhook', [\n 'opportunity_id' => $id,\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n $opportunity->delete();\n DeleteOpportunityJob::dispatch($id)->afterCommit();\n\n return true;\n } catch (Exception $e) {\n $this->logger->error('[HubSpot] Failed to delete opportunity via webhook', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n return false;\n }\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"app ~/jiminny/app, folder","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".circleci, folder","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".cursor, folder","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".github","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".sonarlint, folder","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".vscode, folder","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".windsurf, folder","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"app, sources root","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Actions, folder","depth":8,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Component, folder","depth":8,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Acl, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ActionItems, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Activity, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ActivityAnalytics, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ActivitySearch, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"AiActivityType, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"AiAutomation, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"AiCallScoring, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"AskAnything, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Dtos, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Events, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"AskAnythingPromptService.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"HistoryService.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"AskJiminnyAi, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"AWS, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"BillingManagement, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Cache, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"CoachingFeedback, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Country, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"CustomerApi, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Database, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Datadog, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"DateTime, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"DealInsights, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"DealRisks, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ElasticSearch","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Eloquent","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Encoding","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Encryption","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ES","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Faker","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"FeatureFlags","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"FFMpeg","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"FileSystem","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Gecko","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Gong","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"GuzzleHttp","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"KeyPoints","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Kiosk","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"LanguageDetection","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"LiveFeed","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Locks","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Math","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"MediaPipeline","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"MeetingBot, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"MobileSettings, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Model, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Notification, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Nudge, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ParagraphBreaker, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ParticipantSpeech, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"PartitionedCookie, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"PlaybackPage, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Playlist, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Prophet, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ProphetAi, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ProsperWorks, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Queue, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Job, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"RateLimitAware.php, abstract class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"RateLimitAwareWrapper.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"BotsQueueConstants.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Constants.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ProcessingQueueConstants.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Router, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Saml2, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"SCIM, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Seeder, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Sentry, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Serializer, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Settings, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Sidekick, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Slack, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"TeamInsights, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"TimeMemoryMapper, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Transcription, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"TranscriptionSummary, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Twilio, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Uploader, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"UrlGenerator, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Utility, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Exceptions, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Service, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"BaseRateLimiter.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"EfficientJsonParser.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ProviderRateLimiter.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"RateLimiterInstance.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Uuid, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Waveform, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Webhooks, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Workflow, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Configuration, folder","depth":8,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Console, folder","depth":8,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Commands, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Activities, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Analytics, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Calendars, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Crm, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Hubspot, folder","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"IntegrationApp, folder","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Traits, folder","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"AddLayoutEntities.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"AutologDelayedCommand.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"BullhornCommandAbstract.php, abstract class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"BullhornPingCommand.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"BullhornSearchCommand.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"BullhornSessionCommand.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"CheckActivityLoggableCommand.php, final class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"CleanDuplicateFieldDataCommand.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"FullSyncOpportunityCommand.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"LogActivitiesCommand.php, final class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ManageSyncStrategyCommand.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"MatchCrmObjectsCommand.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"MatchOpportunityActivitiesCommand.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"MigrateProvider.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ProcessHubspotObjectsSyncBatches.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"PurgeDeletedOpportunitiesCommand.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ResetGovernorLimits.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"SendNotLogged.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"SetupActivityTypeForFollowUp.php, final class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"SetupCloseCrm.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"SetupCopperCrm.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"SetupCrmCommand.php, abstract class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"SetupLayouts.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"SyncAccount.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"SyncContact.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"SyncFieldMetadata.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"SyncHubspotActiveDeals.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"SyncHubspotObjects.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"SyncLead.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"SyncObjects.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"SyncOpportunitiesMissingFieldDataCommand.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"SyncOpportunity.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"SyncProfileMetadata.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"SyncTeamMetadata.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"UpdateOpportunitySpecifications.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"DealInsights, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Dev, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"AddRateLimitCommand.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"FixHubSpotTokens.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"FixMissMatchedCrmActivitiesCommand.php, final class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ImportCallsCommand.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"MonitorSocialAccountsState.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Dialers, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"DTOs, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Elasticsearch, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"EngagementStats, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"GeckoExport, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Livestream, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Mailboxes, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Migrate, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"PlaybackThemes, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Playbooks, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Playlists, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Postmark, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ProphetAi, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Reports, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"AutomatedReportsCommand.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"AutomatedReportsRetentionPolicyCommand.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"AutomatedReportsSendCommand.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"CreateMockAskJiminnyReportResultCommand.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"DeleteReportCommand.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"GenerateMarketingReport.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Team.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Usage.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Slack, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Teams, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Tracks, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Transcription, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Twilio, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Users, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Vocabulary, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Zoom, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Command.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"CreateDatabaseUsers.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"DatabaseTableCount.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"DeleteOldAiCrmNotesCommand.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"DeleteS3LeftoversCommand.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"DevPostmanCommand.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"DiarizeViaAiParticipantIdentificationCommand.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"EncryptTokensCommand.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"EngagementStatsRegenerateCommand.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"FeatureFlagsHelper.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"FixCrossTenantIssues.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"FlushRolesPermissionsCache.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"GenerateInternalWebhookToken.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"GroupSetDefaultLanguageCommand.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"HelperTruncateCoachingTables.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"HubspotJournalPollingCommand.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"HubspotWebhookServiceCommand.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ImportRecording.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ImportUsersFromCsvFile.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"IterateUsersCommand.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"JiminnyCacheClearCommand.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"JiminnyDebugCommand.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"JiminnySetEncryptedTokenManagerModeCommand.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"JiminnyTokenInfoCommand.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"MakeSlackLiveCoachingChatNotesOn.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ManageScimForTeam.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"MarkBranchForEnvironmentPipelineCommand.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"MuteOrganizerChannel.php, class","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"PhpApm.php, class","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"PropagateCoachingFeedbackCreatedAtToSectionFeedbacks.php, class","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"PurgeConferences.php, class","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"PurgeSoftDeletedOpportunitiesCommand.php, class","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"PurgeSyncBatchesCommand.php, class","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"RecalculateDealRisksCommand.php, class","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"RemoveDeleteMarkersCommand.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"RemoveExpiredNudgesCommand.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"RemoveUnusedParticipantSpeechesCommand.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ResetElasticSearch.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"RestoreActivityCrmProviderIdCommand.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"RestoreActivityTypeCommand.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"RunAiCallScoringForUntypedActivitiesCommand.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"SeedActivities.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"SendNudgeExpirationWarningsCommand.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"SyncActivity.php, class","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"TrackImported.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"WhichWorkerIsWorkingOnWhichJob.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Scheduling, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Kernel.php","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Contracts, folder","depth":8,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Acl, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ActivitySearch, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"AiAutomation, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Crm, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"DateTime, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ES, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Http, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Requests, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ApiResponse.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"RateLimited.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"RateLimitInterface.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Interactions, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Model, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Nudge, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Playlist, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Repositories, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Services, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"User, folder","depth":9,"on_screen":false,"role_description":"text"}]...
|
4954725836517024779
|
5036074917639293158
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
Editor for custom.log
Sync Changes
Hide This Notification
Code changed:
Hide
Analyzing…
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\ServiceTraits;
use Carbon\Carbon;
use Exception;
use Illuminate\Support\Str;
use Jiminny\Exceptions\CrmException;
use Jiminny\Jobs\Crm\Delete\DeleteAccountJob;
use Jiminny\Jobs\Crm\Delete\DeleteContactJob;
use Jiminny\Jobs\Crm\Delete\DeleteOpportunityJob;
use Jiminny\Models\Account;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Repositories\Crm\CrmEntityRepository;
use Jiminny\Services\Avatar\ProspectPhotoPathService;
use Jiminny\Services\Crm\Hubspot\HubspotClientInterface;
use Jiminny\Services\Crm\Hubspot\WebhookSyncBatchProcessor;
use Jiminny\Utils\StringUtil;
trait SyncCrmEntitiesTrait
{
use OpportunitySyncTrait;
private const string CDN_URL = '[URL_WITH_CREDENTIALS] Carbon $since Fetch contacts modified after this date
* @param Carbon|null $to Optional end date for modification range
*
* @return int Number of contacts successfully synced
*/
public function syncContacts(Carbon $since, ?Carbon $to = null): int
{
$syncCount = 0;
try {
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$fields = $this->getContactFields();
$payload = $this->payloadBuilder->getRecentlyUpdatedSearchPayload($since, $to, $fields);
foreach ($this->client->getPaginatedDataGenerator($payload, 'contacts') as $hsContact) {
$this->importContact($hsContact);
$syncCount++;
}
} catch (Exception $exception) {
$this->logger->error('[' . $this->getDisplayName() . '] Sync contacts failed', [
'teamId' => $this->team->getUuid(),
'reason' => $exception->getMessage(),
]);
}
return $syncCount;
}
/**
* @inheritdoc
*/
public function syncContact(string $crmId): ?Contact
{
try {
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$fields = $this->getContactFields();
$hsContact = $this->client->getContactById($crmId, $fields);
} catch (\HubSpot\Client\Crm\Contacts\ApiException $e) {
$this->logger->info('[' . $this->getDisplayName() . '] Contact fetch failed', [
'teamId' => $this->team->getUuid(),
'crmId' => $crmId,
'reason' => $e->getMessage(),
]);
return null;
} catch (CrmException $e) {
$this->logger->info('[' . $this->getDisplayName() . '] Contact not found', [
'teamId' => $this->team->getUuid(),
'crmId' => $crmId,
'reason' => $e->getMessage(),
]);
return null;
}
if (empty($hsContact['properties']) || empty($hsContact['id'])) {
$this->logger->warning('[' . $this->getDisplayName() . '] Contact data incomplete', [
'teamId' => $this->team->getUuid(),
'crmId' => $crmId,
'has_properties' => ! empty($hsContact['properties']),
'has_id' => ! empty($hsContact['id']),
]);
return null;
}
return $this->importContact($hsContact);
}
private function getContactFields(): array
{
return [
'associatedcompanyid',
'country',
'firstname',
'lastname',
'phone',
'mobilephone',
'email',
'photo',
'hs_avatar_filemanager_key',
'jobtitle',
'hubspot_owner_id',
'hs_object_id',
'createdate',
'hs_lastmodifieddate',
];
}
/**
* @inheritdoc
*/
private function importContact($crmData, array $accountMappings = []): ?Contact
{
$crmProviderId = $crmData['id'] ?? null;
$this->logger->info('[HubSpot] importContact', [
'crm_provider_id' => $crmProviderId,
'config_id' => $this->config->getId(),
]);
if (empty($crmData['properties'])) {
$this->logger->info('[HubSpot] importContact failed: empty properties', [
'crm_provider_id' => $crmProviderId,
'config_id' => $this->config->getId(),
]);
return null;
}
$properties = $crmData['properties'];
$crmId = (string) $crmData['id'];
$accountId = $this->resolveContactAccount($properties, $accountMappings);
$data = $this->buildContactData($crmId, $properties, $accountId);
return $this->crmEntityRepository->importContact($this->config, $data);
}
private function resolveContactAccount(array $properties, array $accountMappings): ?int
{
if (empty($properties['associatedcompanyid'])) {
return null;
}
$companyId = (string) $properties['associatedcompanyid'];
if (! empty($accountMappings)) {
return $accountMappings[$companyId] ?? null;
}
return $this->crmEntityRepository->findAccountByExternalId(
$this->team->getCrmConfiguration(),
$companyId
)?->getId() ?? $this->syncAccount($companyId)?->getId();
}
private function buildContactData(string $crmId, array $properties, ?int $accountId): array
{
$countryCode = $this->buildContactCountry($properties);
$name = $this->buildContactName($properties);
$photoPath = $this->teamService->generateAvatar(
$crmId,
empty($name) ? ($properties['email'] ?? 'N/A') : $name,
);
$parsedNumber = $this->buildContactPhone($countryCode, $properties);
$mobileNumber = $this->buildContactMobilePhone($countryCode, $properties);
$ownerId = $properties['hubspot_owner_id'] ?? null;
$profile = $ownerId !== null
? $this->crmEntityRepository->findProfileByExternalId($this->config, (string) $ownerId)
: null;
$ext = (isset($parsedNumber['ext']) && is_string($parsedNumber['ext']) && strlen($parsedNumber['ext']) <= 10)
? $parsedNumber['ext']
: null;
$title = isset($properties['jobtitle']) ? mb_strimwidth($properties['jobtitle'], 0, 128) : null;
$email = isset($properties['email']) ? mb_strimwidth($properties['email'], 0, 191) : null;
$remotelyCreatedAt = ! empty($properties['createdate']) ? Carbon::parse($properties['createdate']) : null;
return [
'crm_provider_id' => $crmId,
'team_id' => $this->team->getId(),
'account_id' => $accountId,
'user_id' => $profile?->getUserId(),
'owner_id' => $ownerId,
'name' => $name,
'title' => $title,
'email' => $email,
'country_code' => $countryCode,
'phone' => $parsedNumber['phone'] ?? null,
'mobile_phone' => $mobileNumber ?? null,
'ext' => $ext,
'photo_path' => $photoPath,
'remotely_created_at' => $remotelyCreatedAt,
];
}
/**
* @param $properties
*/
private function buildContactName($properties): string
{
if (is_array($properties)) {
return $this->buildContactNameFromArray($properties);
}
return $this->buildContactNameFromObject($properties);
}
private function buildContactNameFromArray(array $properties): string
{
if (! empty($properties['name'])) {
return mb_strimwidth($properties['name'], 0, 100);
}
$name = '';
if (! empty($properties['firstname'])) {
$name = $properties['firstname'] . ' ';
}
if (! empty($properties['lastname'])) {
$name .= $properties['lastname'];
}
if ($name === '' && ! empty($properties['email'])) {
$name = $properties['email'];
}
return mb_strimwidth($name, 0, 100);
}
private function buildContactNameFromObject($properties): string
{
$name = '';
if (isset($properties->firstname)) {
$name = $properties->firstname->value . ' ';
}
if (isset($properties->lastname)) {
$name .= $properties->lastname->value;
}
if ($name === '' && isset($properties->email)) {
$name = $properties->email->value;
}
return mb_strimwidth($name, 0, 100);
}
/**
* @param $properties
*/
private function buildContactPhone(?string $countryCode, $properties): ?array
{
if (is_array($properties) && empty($properties['phone']) === false) {
$number = mb_strimwidth($properties['phone'], 0, 25);
return parsePhoneNumber($countryCode, $number);
} elseif (isset($properties->phone)) {
$number = mb_strimwidth($properties->phone->value, 0, 25);
return parsePhoneNumber($countryCode, $number);
}
return [];
}
/**
* @param $properties
*/
private function buildContactMobilePhone(?string $countryCode, $properties): ?string
{
return isset($properties['mobilephone'])
? Str::limit(phone_e164($countryCode, $properties['mobilephone']), 25, '')
: null;
}
/**
* @param $properties
* @param $account
*/
private function buildContactCountry($properties): ?string
{
if (is_array($properties) && empty($properties['country']) === false) {
return $this->convertCountryNameToCode($properties['country']);
}
if (isset($properties->country)) {
return $this->convertCountryNameToCode($properties->country->value);
}
return null;
}
/**
* HubSpot doesn't have leads, so this method does nothing.
*
* @param Carbon $since
* @param Carbon|null $to
* @param string|null $crmProfileId
*
* @return int
*/
public function syncLeads(Carbon $since, ?Carbon $to = null, ?string $crmProfileId = null): int
{
// Mark unused parameters to avoid code smell warnings
unset($since, $to, $crmProfileId);
return 0;
}
/**
* HubSpot doesn't have leads.
*
* @param string $crmId
*
* @inheritdoc
*/
public function syncLead(string $crmId): ?Lead
{
// Mark unused parameter to avoid code smell warnings
unset($crmId);
return null;
}
/**
* Sync accounts (companies) modified since a given date (manual sync mode).
*
* This method fetches companies from HubSpot API based on modification date and
* imports them one by one. It is used for:
* - Manual sync commands (e.g., crm:sync-account with --from parameter)
* - Initial sync for new teams
* - Backfill operations
*
* For regular sync webhook batchSyncCompanies is used:
*
* @param Carbon $since Fetch companies modified after this date
* @param Carbon|null $to Optional end date for modification range
*
* @return int Number of companies successfully synced
*/
public function syncAccounts(Carbon $since, ?Carbon $to = null): int
{
$syncCount = 0;
try {
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$fields = $this->getCompanyFields();
$payload = $this->payloadBuilder->getRecentlyUpdatedSearchPayload($since, $to, $fields);
foreach ($this->client->getPaginatedDataGenerator($payload, 'companies') as $hsAccount) {
$this->importAccount($hsAccount);
$syncCount++;
}
} catch (Exception $exception) {
$this->logger->error('[' . $this->getDisplayName() . '] Sync accounts failed', [
'teamId' => $this->team->getUuid(),
'reason' => $exception->getMessage(),
]);
}
return $syncCount;
}
/**
* @inheritdoc
*/
public function syncAccount(string $crmId): ?Account
{
try {
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$fields = $this->getCompanyFields();
$hsAccount = $this->client->getAccountById($crmId, $fields);
} catch (\HubSpot\Client\Crm\Companies\ApiException $e) {
$this->logger->info('[' . $this->getDisplayName() . '] Account fetch failed', [
'teamId' => $this->team->getUuid(),
'crmId' => $crmId,
'reason' => $e->getMessage(),
]);
return null;
} catch (CrmException $e) {
$this->logger->info('[' . $this->getDisplayName() . '] Account not found', [
'teamId' => $this->team->getUuid(),
'crmId' => $crmId,
'reason' => $e->getMessage(),
]);
return null;
}
return $this->importAccount($hsAccount);
}
/**
* Process webhook-collected contact batches.
*
* Drains Redis sets containing contact CRM IDs collected from webhook events
* and dispatches ImportContactBatch jobs for batch processing.
*
* @return int Number of contact IDs dispatched to jobs
*/
public function batchSyncContacts(): int
{
$configId = $this->team->getCrmConfiguration()->getId();
return $this->batchProcessor->processBatchesForObjectType(
WebhookSyncBatchProcessor::OBJECT_TYPE_CONTACT,
$configId
);
}
public function importContactBatchByIds(array $crmIds): array
{
$result = [
'success_count' => 0,
'failed_ids' => [],
'errors' => [],
];
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$batchStart = microtime(true);
$slowContacts = [];
$fetchStart = microtime(true);
$allContacts = $this->fetchContactsByIdsInChunks($crmIds);
$fetchMs = (int) round((microtime(true) - $fetchStart) * 1000);
$fetchedIds = array_map('strval', array_column($allContacts, 'id'));
$notFoundIds = array_values(array_diff(array_map('strval', $crmIds), $fetchedIds));
if (! empty($notFoundIds)) {
$this->logger->info('[' . $this->getDisplayName() . '] Contact CRM IDs not found in HubSpot', [
'teamId' => $this->team->getId(),
'notFoundCount' => \count($notFoundIds),
'notFoundIds' => $notFoundIds,
'requestedCount' => \count($crmIds),
'fetchedCount' => \count($allContacts),
]);
}
if (empty($allContacts)) {
return $result;
}
$prepareStart = microtime(true);
$accountMappings = $this->prepareAccountMappingsForContacts($allContacts);
$prepareAccountsMs = (int) round((microtime(true) - $prepareStart) * 1000);
$loopStart = microtime(true);
foreach ($allContacts as $contactData) {
$contactStart = microtime(true);
try {
$contact = $this->importContact($contactData, $accountMappings);
if ($contact !== null) {
$result['success_count']++;
}
} catch (\Throwable $e) {
$crmId = $contactData['id'] ?? 'unknown';
$result['failed_ids'][] = $crmId;
$result['errors'][$crmId] = $e->getMessage();
$this->logger->warning('[' . $this->getDisplayName() . '] Failed to import contact', [
'teamId' => $this->team->getId(),
'crmId' => $crmId,
'error' => $e->getMessage(),
]);
}
$contactMs = (int) round((microtime(true) - $contactStart) * 1000);
if ($contactMs > 1000) {
$slowContacts[] = ['crmId' => $contactData['id'] ?? 'unknown', 'ms' => $contactMs];
}
}
$loopMs = (int) round((microtime(true) - $loopStart) * 1000);
$totalMs = (int) round((microtime(true) - $batchStart) * 1000);
$this->logger->info('[' . $this->getDisplayName() . '] importContactBatch timing', [
'teamId' => $this->team->getId(),
'contact_count' => \count($allContacts),
'requested_count' => \count($crmIds),
'not_found_count' => \count($notFoundIds),
'total_ms' => $totalMs,
'fetch_api_ms' => $fetchMs,
'prepare_accounts_ms' => $prepareAccountsMs,
'contacts_loop_ms' => $loopMs,
'avg_contact_ms' => ! empty($allContacts) ? (int) round($loopMs / \count($allContacts)) : 0,
'slow_contacts_count' => \count($slowContacts),
'slow_contacts' => array_slice($slowContacts, 0, 10),
]);
return $result;
}
private function fetchContactsByIdsInChunks(array $crmIds): array
{
$fields = $this->getContactFields();
$allContacts = [];
foreach (array_chunk($crmIds, self::BATCH_SIZE) as $chunk) {
try {
$contacts = $this->client->getContactsByIds($chunk, $fields);
foreach ($contacts as $contactData) {
$allContacts[] = $contactData;
}
} catch (\Throwable $e) {
// @TODO what will happen if this exception is thrown
$this->logger->warning('[' . $this->getDisplayName() . '] Batch contact fetch failed', [
'chunk_size' => \count($chunk),
'error' => $e->getMessage(),
]);
throw $e;
}
}
return $allContacts;
}
private function prepareAccountMappingsForContacts(array $contacts): array
{
$companyIds = [];
foreach ($contacts as $contact) {
$companyId = $contact['properties']['associatedcompanyid'] ?? null;
if ($companyId !== null && $companyId !== '') {
$companyIds[] = (string) $companyId;
}
}
$companyIds = array_unique($companyIds);
if (empty($companyIds)) {
return [];
}
$mappings = $this->crmEntityRepository
->getExistingAccountIdsMap($this->config, $companyIds);
$missingCompanyIds = array_diff($companyIds, array_keys($mappings));
if (empty($missingCompanyIds)) {
return $mappings;
}
$this->logger->info('[' . $this->getDisplayName() . '] Batch syncing missing accounts for contacts', [
'teamId' => $this->team->getId(),
'total_companies' => \count($companyIds),
'existing_companies' => \count($mappings),
'missing_companies' => \count($missingCompanyIds),
]);
try {
$syncedAccounts = $this->batchSyncAccountsForContacts($missingCompanyIds);
$mappings = array_merge($mappings, $syncedAccounts);
} catch (\Throwable $e) {
$this->logger->error('[' . $this->getDisplayName() . '] Failed to batch sync missing accounts', [
'teamId' => $this->team->getId(),
'missingCompanyIds' => $missingCompanyIds,
'missingCount' => count($missingCompanyIds),
'error' => $e->getMessage(),
]);
}
return $mappings;
}
private function batchSyncAccountsForContacts(array $companyIds): array
{
$syncedAccounts = [];
$fields = $this->getCompanyFields();
foreach (array_chunk($companyIds, self::BATCH_SIZE) as $chunk) {
try {
$companies = $this->client->getCompaniesByIds($chunk, $fields);
foreach ($companies as $companyData) {
try {
$account = $this->importAccount($companyData);
if ($account) {
$syncedAccounts[$account->getCrmProviderId()] = $account->getId();
}
} catch (\Throwable $e) {
$this->logger->warning('[' . $this->getDisplayName() . '] Failed to import account in batch', [
'teamId' => $this->team->getId(),
'companyId' => $companyData['id'] ?? 'unknown',
'error' => $e->getMessage(),
]);
}
}
} catch (\Throwable $e) {
$this->logger->warning('[' . $this->getDisplayName() . '] Batch company fetch failed', [
'teamId' => $this->team->getId(),
'chunk_size' => \count($chunk),
'error' => $e->getMessage(),
]);
}
}
return $syncedAccounts;
}
/**
* Process webhook-collected company batches.
*
* Drains Redis sets containing company CRM IDs collected from webhook events
* and dispatches ImportAccountBatch jobs for batch processing.
*
* @return int Number of company IDs dispatched to jobs
*/
public function batchSyncCompanies(): int
{
$configId = $this->team->getCrmConfiguration()->getId();
return $this->batchProcessor->processBatchesForObjectType(
WebhookSyncBatchProcessor::OBJECT_TYPE_COMPANY,
$configId
);
}
public function importAccountBatchByIds(array $crmIds): array
{
$result = [
'success_count' => 0,
'failed_ids' => [],
'errors' => [],
];
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$batchStart = microtime(true);
$slowAccounts = [];
$fields = $this->getCompanyFields();
$allCompanies = [];
$fetchStart = microtime(true);
foreach (array_chunk($crmIds, self::BATCH_SIZE) as $chunk) {
try {
$companies = $this->client->getCompaniesByIds($chunk, $fields);
foreach ($companies as $companyData) {
$allCompanies[] = $companyData;
}
} catch (\Throwable $e) {
$this->logger->warning('[' . $this->getDisplayName() . '] Batch company fetch failed', [
'chunk_size' => \count($chunk),
'error' => $e->getMessage(),
]);
throw $e;
}
}
$fetchMs = (int) round((microtime(true) - $fetchStart) * 1000);
$fetchedIds = array_map('strval', array_column($allCompanies, 'id'));
$notFoundIds = array_values(array_diff(array_map('strval', $crmIds), $fetchedIds));
if (! empty($notFoundIds)) {
$this->logger->info('[' . $this->getDisplayName() . '] Company CRM IDs not found in HubSpot', [
'teamId' => $this->team->getId(),
'notFoundCount' => \count($notFoundIds),
'notFoundIds' => $notFoundIds,
'requestedCount' => \count($crmIds),
'fetchedCount' => \count($allCompanies),
]);
}
$loopStart = microtime(true);
foreach ($allCompanies as $companyData) {
$accountStart = microtime(true);
try {
$account = $this->importAccount($companyData);
if ($account !== null) {
$result['success_count']++;
}
} catch (\Throwable $e) {
$crmId = $companyData['id'] ?? $companyData['properties']['hs_object_id'] ?? 'unknown';
$result['failed_ids'][] = $crmId;
$result['errors'][$crmId] = $e->getMessage();
$this->logger->warning('[' . $this->getDisplayName() . '] Failed to import account', [
'crmId' => $crmId,
'error' => $e->getMessage(),
]);
}
$accountMs = (int) round((microtime(true) - $accountStart) * 1000);
if ($accountMs > 1000) {
$crmId = $companyData['id'] ?? $companyData['properties']['hs_object_id'] ?? 'unknown';
$slowAccounts[] = ['crmId' => $crmId, 'ms' => $accountMs];
}
}
$loopMs = (int) round((microtime(true) - $loopStart) * 1000);
$totalMs = (int) round((microtime(true) - $batchStart) * 1000);
$this->logger->info('[' . $this->getDisplayName() . '] importAccountBatch timing', [
'teamId' => $this->team->getId(),
'account_count' => \count($allCompanies),
'requested_count' => \count($crmIds),
'not_found_count' => \count($notFoundIds),
'total_ms' => $totalMs,
'fetch_api_ms' => $fetchMs,
'accounts_loop_ms' => $loopMs,
'avg_account_ms' => ! empty($allCompanies) ? (int) round($loopMs / \count($allCompanies)) : 0,
'slow_accounts_count' => \count($slowAccounts),
'slow_accounts' => array_slice($slowAccounts, 0, 10),
]);
return $result;
}
private function getCompanyFields(): array
{
return [
'country',
'name',
'phone',
'domain',
'hs_avatar_filemanager_key',
'industry',
'hubspot_owner_id',
'hs_object_id',
'createdate',
'hs_lastmodifieddate',
];
}
private function importAccount($crmData): ?Account
{
$crmProviderId = $crmData['id'] ?? $crmData['properties']['hs_object_id'] ?? null;
$this->logger->info('[HubSpot] importAccount', [
'crm_provider_id' => $crmProviderId,
'config_id' => $this->config->getId(),
]);
if (empty($crmData['properties'])) {
$this->logger->info('[HubSpot] importAccount failed: empty properties', [
'crm_provider_id' => $crmProviderId,
'config_id' => $this->config->getId(),
]);
return null;
}
$properties = $crmData['properties'];
$crmId = (string) $properties['hs_object_id'];
$countryCode = isset($properties['country']) ? $this->convertCountryNameToCode($properties['country']) : null;
if (isset($properties['phone'])) {
// Trim to our width and attempt to parse it.
$number = mb_strimwidth($properties['phone'], 0, 25);
$parsedNumber = parsePhoneNumber($countryCode, $number);
} else {
$parsedNumber = [];
}
$name = '[unknown]';
if (isset($properties['name'])) {
$name = $properties['name'];
}
$photoPath = $this->prospectPhotoPathService->getOrGeneratePhotoPath(
$this->config,
$crmId,
Account::class,
$crmId,
$name
);
$industry = null;
if (isset($properties['industry'])) {
$industry = mb_strimwidth($properties['industry'], 0, 40);
}
$ownerId = $profile = null;
if (isset($properties['hubspot_owner_id'])) {
$ownerId = $properties['hubspot_owner_id'];
$profile = $this->crmEntityRepository->findProfileByExternalId($this->config, (string) $ownerId);
}
$domain = null;
if (isset($properties['domain'])) {
$domain = StringUtil::resolveDomain($properties['domain']);
}
$remotelyCreatedAt = null;
if (isset($properties['createdate']) && ! empty($properties['createdate'])) {
$remotelyCreatedAt = Carbon::parse($properties['createdate']);
}
$data = [
'crm_provider_id' => $crmId,
'team_id' => $this->team->id,
'user_id' => $profile ? $profile->user_id : null,
'owner_id' => $ownerId,
'name' => mb_strimwidth($name, 0, 191),
'photo_path' => $photoPath,
'industry' => $industry,
'domain' => $domain !== null
? substr($domain, 0, 191)
: null,
'phone' => $parsedNumber['phone'] ?? null,
'ext' => $parsedNumber['ext'] ?? null,
'country_code' => $countryCode,
'remotely_created_at' => $remotelyCreatedAt,
];
return $this->crmEntityRepository->importAccount($this->config, $data);
}
public function deleteContact(string $crmProviderId): bool
{
try {
$contact = $this->crmEntityRepository->findContactByExternalId($this->config, $crmProviderId);
if (! $contact) {
$this->logger->info('[HubSpot] Contact not found for deletion', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
// return success because we do not know which instance is the target
return true;
}
$id = $contact->getId();
$this->logger->info('[HubSpot] Deleting contact via webhook', [
'contact_id' => $id,
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
$contact->delete();
DeleteContactJob::dispatch($id)->afterCommit();
return true;
} catch (Exception $e) {
$this->logger->error('[HubSpot] Failed to delete contact via webhook', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
'error' => $e->getMessage(),
]);
return false;
}
}
public function deleteAccount(string $crmProviderId): bool
{
try {
$account = $this->crmEntityRepository->findAccountByExternalId($this->config, $crmProviderId);
if (! $account) {
$this->logger->info('[HubSpot] Account not found for deletion', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
// return success because we do not know which instance is the target
return true;
}
$id = $account->getId();
$this->logger->info('[HubSpot] Deleting account via webhook', [
'account_id' => $id,
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
$account->delete();
DeleteAccountJob::dispatch($id)->afterCommit();
return true;
} catch (Exception $e) {
$this->logger->error('[HubSpot] Failed to delete account via webhook', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
'error' => $e->getMessage(),
]);
return false;
}
}
public function deleteOpportunity(string $crmProviderId): bool
{
try {
$opportunity = $this->crmEntityRepository->findOpportunityByExternalId($this->config, $crmProviderId);
if (! $opportunity) {
$this->logger->info('[HubSpot] Opportunity not found for deletion', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
// return success because we do not know which instance is the target
return true;
}
$id = $opportunity->getId();
$this->logger->info('[HubSpot] Deleting opportunity via webhook', [
'opportunity_id' => $id,
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
$opportunity->delete();
DeleteOpportunityJob::dispatch($id)->afterCommit();
return true;
} catch (Exception $e) {
$this->logger->error('[HubSpot] Failed to delete opportunity via webhook', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
'error' => $e->getMessage(),
]);
return false;
}
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide
app ~/jiminny/app, folder
.circleci, folder
.cursor, folder
.github
.sonarlint, folder
.vscode, folder
.windsurf, folder
app, sources root
Actions, folder
Component, folder
Acl, folder
ActionItems, folder
Activity, folder
ActivityAnalytics, folder
ActivitySearch, folder
AiActivityType, folder
AiAutomation, folder
AiCallScoring, folder
AskAnything, folder
Dtos, folder
Events, folder
AskAnythingPromptService.php
HistoryService.php
AskJiminnyAi, folder
AWS, folder
BillingManagement, folder
Cache, folder
CoachingFeedback, folder
Country, folder
CustomerApi, folder
Database, folder
Datadog, folder
DateTime, folder
DealInsights, folder
DealRisks, folder
ElasticSearch
Eloquent
Encoding
Encryption
ES
Faker
FeatureFlags
FFMpeg
FileSystem
Gecko
Gong
GuzzleHttp
KeyPoints
Kiosk
LanguageDetection
LiveFeed
Locks
Math
MediaPipeline
MeetingBot, folder
MobileSettings, folder
Model, folder
Notification, folder
Nudge, folder
ParagraphBreaker, folder
ParticipantSpeech, folder
PartitionedCookie, folder
PlaybackPage, folder
Playlist, folder
Prophet, folder
ProphetAi, folder
ProsperWorks, folder
Queue, folder
Job, folder
RateLimitAware.php, abstract class
RateLimitAwareWrapper.php, class
BotsQueueConstants.php
Constants.php
ProcessingQueueConstants.php
Router, folder
Saml2, folder
SCIM, folder
Seeder, folder
Sentry, folder
Serializer, folder
Settings, folder
Sidekick, folder
Slack, folder
TeamInsights, folder
TimeMemoryMapper, folder
Transcription, folder
TranscriptionSummary, folder
Twilio, folder
Uploader, folder
UrlGenerator, folder
Utility, folder
Exceptions, folder
Service, folder
BaseRateLimiter.php, class
EfficientJsonParser.php, class
ProviderRateLimiter.php, class
RateLimiterInstance.php, class
Uuid, folder
Waveform, folder
Webhooks, folder
Workflow, folder
Configuration, folder
Console, folder
Commands, folder
Activities, folder
Analytics, folder
Calendars, folder
Crm, folder
Hubspot, folder
IntegrationApp, folder
Traits, folder
AddLayoutEntities.php, class
AutologDelayedCommand.php, class
BullhornCommandAbstract.php, abstract class
BullhornPingCommand.php, class
BullhornSearchCommand.php, class
BullhornSessionCommand.php, class
CheckActivityLoggableCommand.php, final class
CleanDuplicateFieldDataCommand.php, class
FullSyncOpportunityCommand.php, class
LogActivitiesCommand.php, final class
ManageSyncStrategyCommand.php, class
MatchCrmObjectsCommand.php, class
MatchOpportunityActivitiesCommand.php, class
MigrateProvider.php, class
ProcessHubspotObjectsSyncBatches.php, class
PurgeDeletedOpportunitiesCommand.php, class
ResetGovernorLimits.php, class
SendNotLogged.php, class
SetupActivityTypeForFollowUp.php, final class
SetupCloseCrm.php, class
SetupCopperCrm.php, class
SetupCrmCommand.php, abstract class
SetupLayouts.php, class
SyncAccount.php, class
SyncContact.php, class
SyncFieldMetadata.php, class
SyncHubspotActiveDeals.php, class
SyncHubspotObjects.php, class
SyncLead.php, class
SyncObjects.php, class
SyncOpportunitiesMissingFieldDataCommand.php, class
SyncOpportunity.php, class
SyncProfileMetadata.php, class
SyncTeamMetadata.php, class
UpdateOpportunitySpecifications.php, class
DealInsights, folder
Dev, folder
AddRateLimitCommand.php, class
FixHubSpotTokens.php, class
FixMissMatchedCrmActivitiesCommand.php, final class
ImportCallsCommand.php, class
MonitorSocialAccountsState.php, class
Dialers, folder
DTOs, folder
Elasticsearch, folder
EngagementStats, folder
GeckoExport, folder
Livestream, folder
Mailboxes, folder
Migrate, folder
PlaybackThemes, folder
Playbooks, folder
Playlists, folder
Postmark, folder
ProphetAi, folder
Reports, folder
AutomatedReportsCommand.php, class
AutomatedReportsRetentionPolicyCommand.php, class
AutomatedReportsSendCommand.php, class
CreateMockAskJiminnyReportResultCommand.php, class
DeleteReportCommand.php, class
GenerateMarketingReport.php, class
Team.php, class
Usage.php, class
Slack, folder
Teams, folder
Tracks, folder
Transcription, folder
Twilio, folder
Users, folder
Vocabulary, folder
Zoom, folder
Command.php
CreateDatabaseUsers.php
DatabaseTableCount.php
DeleteOldAiCrmNotesCommand.php
DeleteS3LeftoversCommand.php
DevPostmanCommand.php
DiarizeViaAiParticipantIdentificationCommand.php
EncryptTokensCommand.php
EngagementStatsRegenerateCommand.php
FeatureFlagsHelper.php
FixCrossTenantIssues.php
FlushRolesPermissionsCache.php
GenerateInternalWebhookToken.php
GroupSetDefaultLanguageCommand.php
HelperTruncateCoachingTables.php
HubspotJournalPollingCommand.php
HubspotWebhookServiceCommand.php
ImportRecording.php
ImportUsersFromCsvFile.php
IterateUsersCommand.php
JiminnyCacheClearCommand.php
JiminnyDebugCommand.php
JiminnySetEncryptedTokenManagerModeCommand.php
JiminnyTokenInfoCommand.php
MakeSlackLiveCoachingChatNotesOn.php
ManageScimForTeam.php
MarkBranchForEnvironmentPipelineCommand.php
MuteOrganizerChannel.php, class
PhpApm.php, class
PropagateCoachingFeedbackCreatedAtToSectionFeedbacks.php, class
PurgeConferences.php, class
PurgeSoftDeletedOpportunitiesCommand.php, class
PurgeSyncBatchesCommand.php, class
RecalculateDealRisksCommand.php, class
RemoveDeleteMarkersCommand.php
RemoveExpiredNudgesCommand.php
RemoveUnusedParticipantSpeechesCommand.php
ResetElasticSearch.php
RestoreActivityCrmProviderIdCommand.php
RestoreActivityTypeCommand.php
RunAiCallScoringForUntypedActivitiesCommand.php
SeedActivities.php
SendNudgeExpirationWarningsCommand.php
SyncActivity.php, class
TrackImported.php
WhichWorkerIsWorkingOnWhichJob.php
Scheduling, folder
Kernel.php
Contracts, folder
Acl, folder
ActivitySearch, folder
AiAutomation, folder
Crm, folder
DateTime, folder
ES, folder
Http, folder
Requests, folder
ApiResponse.php
RateLimited.php
RateLimitInterface.php
Interactions, folder
Model, folder
Nudge, folder
Playlist, folder
Repositories, folder
Services, folder
User, folder...
|
2841
|
NULL
|
NULL
|
NULL
|
|
2842
|
113
|
38
|
2026-05-07T11:43:03.885891+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778154183885_m1.jpg...
|
PhpStorm
|
faVsco.js – SyncCrmEntitiesTrait.php
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
Editor for custom.log
Sync Changes
Hide This Notification
Code changed:
Hide
Analyzing…
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\ServiceTraits;
use Carbon\Carbon;
use Exception;
use Illuminate\Support\Str;
use Jiminny\Exceptions\CrmException;
use Jiminny\Jobs\Crm\Delete\DeleteAccountJob;
use Jiminny\Jobs\Crm\Delete\DeleteContactJob;
use Jiminny\Jobs\Crm\Delete\DeleteOpportunityJob;
use Jiminny\Models\Account;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Repositories\Crm\CrmEntityRepository;
use Jiminny\Services\Avatar\ProspectPhotoPathService;
use Jiminny\Services\Crm\Hubspot\HubspotClientInterface;
use Jiminny\Services\Crm\Hubspot\WebhookSyncBatchProcessor;
use Jiminny\Utils\StringUtil;
trait SyncCrmEntitiesTrait
{
use OpportunitySyncTrait;
private const string CDN_URL = '[URL_WITH_CREDENTIALS] Carbon $since Fetch contacts modified after this date
* @param Carbon|null $to Optional end date for modification range
*
* @return int Number of contacts successfully synced
*/
public function syncContacts(Carbon $since, ?Carbon $to = null): int
{
$syncCount = 0;
try {
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$fields = $this->getContactFields();
$payload = $this->payloadBuilder->getRecentlyUpdatedSearchPayload($since, $to, $fields);
foreach ($this->client->getPaginatedDataGenerator($payload, 'contacts') as $hsContact) {
$this->importContact($hsContact);
$syncCount++;
}
} catch (Exception $exception) {
$this->logger->error('[' . $this->getDisplayName() . '] Sync contacts failed', [
'teamId' => $this->team->getUuid(),
'reason' => $exception->getMessage(),
]);
}
return $syncCount;
}
/**
* @inheritdoc
*/
public function syncContact(string $crmId): ?Contact
{
try {
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$fields = $this->getContactFields();
$hsContact = $this->client->getContactById($crmId, $fields);
} catch (\HubSpot\Client\Crm\Contacts\ApiException $e) {
$this->logger->info('[' . $this->getDisplayName() . '] Contact fetch failed', [
'teamId' => $this->team->getUuid(),
'crmId' => $crmId,
'reason' => $e->getMessage(),
]);
return null;
} catch (CrmException $e) {
$this->logger->info('[' . $this->getDisplayName() . '] Contact not found', [
'teamId' => $this->team->getUuid(),
'crmId' => $crmId,
'reason' => $e->getMessage(),
]);
return null;
}
if (empty($hsContact['properties']) || empty($hsContact['id'])) {
$this->logger->warning('[' . $this->getDisplayName() . '] Contact data incomplete', [
'teamId' => $this->team->getUuid(),
'crmId' => $crmId,
'has_properties' => ! empty($hsContact['properties']),
'has_id' => ! empty($hsContact['id']),
]);
return null;
}
return $this->importContact($hsContact);
}
private function getContactFields(): array
{
return [
'associatedcompanyid',
'country',
'firstname',
'lastname',
'phone',
'mobilephone',
'email',
'photo',
'hs_avatar_filemanager_key',
'jobtitle',
'hubspot_owner_id',
'hs_object_id',
'createdate',
'hs_lastmodifieddate',
];
}
/**
* @inheritdoc
*/
private function importContact($crmData, array $accountMappings = []): ?Contact
{
$crmProviderId = $crmData['id'] ?? null;
$this->logger->info('[HubSpot] importContact', [
'crm_provider_id' => $crmProviderId,
'config_id' => $this->config->getId(),
]);
if (empty($crmData['properties'])) {
$this->logger->info('[HubSpot] importContact failed: empty properties', [
'crm_provider_id' => $crmProviderId,
'config_id' => $this->config->getId(),
]);
return null;
}
$properties = $crmData['properties'];
$crmId = (string) $crmData['id'];
$accountId = $this->resolveContactAccount($properties, $accountMappings);
$data = $this->buildContactData($crmId, $properties, $accountId);
return $this->crmEntityRepository->importContact($this->config, $data);
}
private function resolveContactAccount(array $properties, array $accountMappings): ?int
{
if (empty($properties['associatedcompanyid'])) {
return null;
}
$companyId = (string) $properties['associatedcompanyid'];
if (! empty($accountMappings)) {
return $accountMappings[$companyId] ?? null;
}
return $this->crmEntityRepository->findAccountByExternalId(
$this->team->getCrmConfiguration(),
$companyId
)?->getId() ?? $this->syncAccount($companyId)?->getId();
}
private function buildContactData(string $crmId, array $properties, ?int $accountId): array
{
$countryCode = $this->buildContactCountry($properties);
$name = $this->buildContactName($properties);
$photoPath = $this->teamService->generateAvatar(
$crmId,
empty($name) ? ($properties['email'] ?? 'N/A') : $name,
);
$parsedNumber = $this->buildContactPhone($countryCode, $properties);
$mobileNumber = $this->buildContactMobilePhone($countryCode, $properties);
$ownerId = $properties['hubspot_owner_id'] ?? null;
$profile = $ownerId !== null
? $this->crmEntityRepository->findProfileByExternalId($this->config, (string) $ownerId)
: null;
$ext = (isset($parsedNumber['ext']) && is_string($parsedNumber['ext']) && strlen($parsedNumber['ext']) <= 10)
? $parsedNumber['ext']
: null;
$title = isset($properties['jobtitle']) ? mb_strimwidth($properties['jobtitle'], 0, 128) : null;
$email = isset($properties['email']) ? mb_strimwidth($properties['email'], 0, 191) : null;
$remotelyCreatedAt = ! empty($properties['createdate']) ? Carbon::parse($properties['createdate']) : null;
return [
'crm_provider_id' => $crmId,
'team_id' => $this->team->getId(),
'account_id' => $accountId,
'user_id' => $profile?->getUserId(),
'owner_id' => $ownerId,
'name' => $name,
'title' => $title,
'email' => $email,
'country_code' => $countryCode,
'phone' => $parsedNumber['phone'] ?? null,
'mobile_phone' => $mobileNumber ?? null,
'ext' => $ext,
'photo_path' => $photoPath,
'remotely_created_at' => $remotelyCreatedAt,
];
}
/**
* @param $properties
*/
private function buildContactName($properties): string
{
if (is_array($properties)) {
return $this->buildContactNameFromArray($properties);
}
return $this->buildContactNameFromObject($properties);
}
private function buildContactNameFromArray(array $properties): string
{
if (! empty($properties['name'])) {
return mb_strimwidth($properties['name'], 0, 100);
}
$name = '';
if (! empty($properties['firstname'])) {
$name = $properties['firstname'] . ' ';
}
if (! empty($properties['lastname'])) {
$name .= $properties['lastname'];
}
if ($name === '' && ! empty($properties['email'])) {
$name = $properties['email'];
}
return mb_strimwidth($name, 0, 100);
}
private function buildContactNameFromObject($properties): string
{
$name = '';
if (isset($properties->firstname)) {
$name = $properties->firstname->value . ' ';
}
if (isset($properties->lastname)) {
$name .= $properties->lastname->value;
}
if ($name === '' && isset($properties->email)) {
$name = $properties->email->value;
}
return mb_strimwidth($name, 0, 100);
}
/**
* @param $properties
*/
private function buildContactPhone(?string $countryCode, $properties): ?array
{
if (is_array($properties) && empty($properties['phone']) === false) {
$number = mb_strimwidth($properties['phone'], 0, 25);
return parsePhoneNumber($countryCode, $number);
} elseif (isset($properties->phone)) {
$number = mb_strimwidth($properties->phone->value, 0, 25);
return parsePhoneNumber($countryCode, $number);
}
return [];
}
/**
* @param $properties
*/
private function buildContactMobilePhone(?string $countryCode, $properties): ?string
{
return isset($properties['mobilephone'])
? Str::limit(phone_e164($countryCode, $properties['mobilephone']), 25, '')
: null;
}
/**
* @param $properties
* @param $account
*/
private function buildContactCountry($properties): ?string
{
if (is_array($properties) && empty($properties['country']) === false) {
return $this->convertCountryNameToCode($properties['country']);
}
if (isset($properties->country)) {
return $this->convertCountryNameToCode($properties->country->value);
}
return null;
}
/**
* HubSpot doesn't have leads, so this method does nothing.
*
* @param Carbon $since
* @param Carbon|null $to
* @param string|null $crmProfileId
*
* @return int
*/
public function syncLeads(Carbon $since, ?Carbon $to = null, ?string $crmProfileId = null): int
{
// Mark unused parameters to avoid code smell warnings
unset($since, $to, $crmProfileId);
return 0;
}
/**
* HubSpot doesn't have leads.
*
* @param string $crmId
*
* @inheritdoc
*/
public function syncLead(string $crmId): ?Lead
{
// Mark unused parameter to avoid code smell warnings
unset($crmId);
return null;
}
/**
* Sync accounts (companies) modified since a given date (manual sync mode).
*
* This method fetches companies from HubSpot API based on modification date and
* imports them one by one. It is used for:
* - Manual sync commands (e.g., crm:sync-account with --from parameter)
* - Initial sync for new teams
* - Backfill operations
*
* For regular sync webhook batchSyncCompanies is used:
*
* @param Carbon $since Fetch companies modified after this date
* @param Carbon|null $to Optional end date for modification range
*
* @return int Number of companies successfully synced
*/
public function syncAccounts(Carbon $since, ?Carbon $to = null): int
{
$syncCount = 0;
try {
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$fields = $this->getCompanyFields();
$payload = $this->payloadBuilder->getRecentlyUpdatedSearchPayload($since, $to, $fields);
foreach ($this->client->getPaginatedDataGenerator($payload, 'companies') as $hsAccount) {
$this->importAccount($hsAccount);
$syncCount++;
}
} catch (Exception $exception) {
$this->logger->error('[' . $this->getDisplayName() . '] Sync accounts failed', [
'teamId' => $this->team->getUuid(),
'reason' => $exception->getMessage(),
]);
}
return $syncCount;
}
/**
* @inheritdoc
*/
public function syncAccount(string $crmId): ?Account
{
try {
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$fields = $this->getCompanyFields();
$hsAccount = $this->client->getAccountById($crmId, $fields);
} catch (\HubSpot\Client\Crm\Companies\ApiException $e) {
$this->logger->info('[' . $this->getDisplayName() . '] Account fetch failed', [
'teamId' => $this->team->getUuid(),
'crmId' => $crmId,
'reason' => $e->getMessage(),
]);
return null;
} catch (CrmException $e) {
$this->logger->info('[' . $this->getDisplayName() . '] Account not found', [
'teamId' => $this->team->getUuid(),
'crmId' => $crmId,
'reason' => $e->getMessage(),
]);
return null;
}
return $this->importAccount($hsAccount);
}
/**
* Process webhook-collected contact batches.
*
* Drains Redis sets containing contact CRM IDs collected from webhook events
* and dispatches ImportContactBatch jobs for batch processing.
*
* @return int Number of contact IDs dispatched to jobs
*/
public function batchSyncContacts(): int
{
$configId = $this->team->getCrmConfiguration()->getId();
return $this->batchProcessor->processBatchesForObjectType(
WebhookSyncBatchProcessor::OBJECT_TYPE_CONTACT,
$configId
);
}
public function importContactBatchByIds(array $crmIds): array
{
$result = [
'success_count' => 0,
'failed_ids' => [],
'errors' => [],
];
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$batchStart = microtime(true);
$slowContacts = [];
$fetchStart = microtime(true);
$allContacts = $this->fetchContactsByIdsInChunks($crmIds);
$fetchMs = (int) round((microtime(true) - $fetchStart) * 1000);
$fetchedIds = array_map('strval', array_column($allContacts, 'id'));
$notFoundIds = array_values(array_diff(array_map('strval', $crmIds), $fetchedIds));
if (! empty($notFoundIds)) {
$this->logger->info('[' . $this->getDisplayName() . '] Contact CRM IDs not found in HubSpot', [
'teamId' => $this->team->getId(),
'notFoundCount' => \count($notFoundIds),
'notFoundIds' => $notFoundIds,
'requestedCount' => \count($crmIds),
'fetchedCount' => \count($allContacts),
]);
}
if (empty($allContacts)) {
return $result;
}
$prepareStart = microtime(true);
$accountMappings = $this->prepareAccountMappingsForContacts($allContacts);
$prepareAccountsMs = (int) round((microtime(true) - $prepareStart) * 1000);
$loopStart = microtime(true);
foreach ($allContacts as $contactData) {
$contactStart = microtime(true);
try {
$contact = $this->importContact($contactData, $accountMappings);
if ($contact !== null) {
$result['success_count']++;
}
} catch (\Throwable $e) {
$crmId = $contactData['id'] ?? 'unknown';
$result['failed_ids'][] = $crmId;
$result['errors'][$crmId] = $e->getMessage();
$this->logger->warning('[' . $this->getDisplayName() . '] Failed to import contact', [
'teamId' => $this->team->getId(),
'crmId' => $crmId,
'error' => $e->getMessage(),
]);
}
$contactMs = (int) round((microtime(true) - $contactStart) * 1000);
if ($contactMs > 1000) {
$slowContacts[] = ['crmId' => $contactData['id'] ?? 'unknown', 'ms' => $contactMs];
}
}
$loopMs = (int) round((microtime(true) - $loopStart) * 1000);
$totalMs = (int) round((microtime(true) - $batchStart) * 1000);
$this->logger->info('[' . $this->getDisplayName() . '] importContactBatch timing', [
'teamId' => $this->team->getId(),
'contact_count' => \count($allContacts),
'requested_count' => \count($crmIds),
'not_found_count' => \count($notFoundIds),
'total_ms' => $totalMs,
'fetch_api_ms' => $fetchMs,
'prepare_accounts_ms' => $prepareAccountsMs,
'contacts_loop_ms' => $loopMs,
'avg_contact_ms' => ! empty($allContacts) ? (int) round($loopMs / \count($allContacts)) : 0,
'slow_contacts_count' => \count($slowContacts),
'slow_contacts' => array_slice($slowContacts, 0, 10),
]);
return $result;
}
private function fetchContactsByIdsInChunks(array $crmIds): array
{
$fields = $this->getContactFields();
$allContacts = [];
foreach (array_chunk($crmIds, self::BATCH_SIZE) as $chunk) {
try {
$contacts = $this->client->getContactsByIds($chunk, $fields);
foreach ($contacts as $contactData) {
$allContacts[] = $contactData;
}
} catch (\Throwable $e) {
// @TODO what will happen if this exception is thrown
$this->logger->warning('[' . $this->getDisplayName() . '] Batch contact fetch failed', [
'chunk_size' => \count($chunk),
'error' => $e->getMessage(),
]);
throw $e;
}
}
return $allContacts;
}
private function prepareAccountMappingsForContacts(array $contacts): array
{
$companyIds = [];
foreach ($contacts as $contact) {
$companyId = $contact['properties']['associatedcompanyid'] ?? null;
if ($companyId !== null && $companyId !== '') {
$companyIds[] = (string) $companyId;
}
}
$companyIds = array_unique($companyIds);
if (empty($companyIds)) {
return [];
}
$mappings = $this->crmEntityRepository
->getExistingAccountIdsMap($this->config, $companyIds);
$missingCompanyIds = array_diff($companyIds, array_keys($mappings));
if (empty($missingCompanyIds)) {
return $mappings;
}
$this->logger->info('[' . $this->getDisplayName() . '] Batch syncing missing accounts for contacts', [
'teamId' => $this->team->getId(),
'total_companies' => \count($companyIds),
'existing_companies' => \count($mappings),
'missing_companies' => \count($missingCompanyIds),
]);
try {
$syncedAccounts = $this->batchSyncAccountsForContacts($missingCompanyIds);
$mappings = array_merge($mappings, $syncedAccounts);
} catch (\Throwable $e) {
$this->logger->error('[' . $this->getDisplayName() . '] Failed to batch sync missing accounts', [
'teamId' => $this->team->getId(),
'missingCompanyIds' => $missingCompanyIds,
'missingCount' => count($missingCompanyIds),
'error' => $e->getMessage(),
]);
}
return $mappings;
}
private function batchSyncAccountsForContacts(array $companyIds): array
{
$syncedAccounts = [];
$fields = $this->getCompanyFields();
foreach (array_chunk($companyIds, self::BATCH_SIZE) as $chunk) {
try {
$companies = $this->client->getCompaniesByIds($chunk, $fields);
foreach ($companies as $companyData) {
try {
$account = $this->importAccount($companyData);
if ($account) {
$syncedAccounts[$account->getCrmProviderId()] = $account->getId();
}
} catch (\Throwable $e) {
$this->logger->warning('[' . $this->getDisplayName() . '] Failed to import account in batch', [
'teamId' => $this->team->getId(),
'companyId' => $companyData['id'] ?? 'unknown',
'error' => $e->getMessage(),
]);
}
}
} catch (\Throwable $e) {
$this->logger->warning('[' . $this->getDisplayName() . '] Batch company fetch failed', [
'teamId' => $this->team->getId(),
'chunk_size' => \count($chunk),
'error' => $e->getMessage(),
]);
}
}
return $syncedAccounts;
}
/**
* Process webhook-collected company batches.
*
* Drains Redis sets containing company CRM IDs collected from webhook events
* and dispatches ImportAccountBatch jobs for batch processing.
*
* @return int Number of company IDs dispatched to jobs
*/
public function batchSyncCompanies(): int
{
$configId = $this->team->getCrmConfiguration()->getId();
return $this->batchProcessor->processBatchesForObjectType(
WebhookSyncBatchProcessor::OBJECT_TYPE_COMPANY,
$configId
);
}
public function importAccountBatchByIds(array $crmIds): array
{
$result = [
'success_count' => 0,
'failed_ids' => [],
'errors' => [],
];
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$batchStart = microtime(true);
$slowAccounts = [];
$fields = $this->getCompanyFields();
$allCompanies = [];
$fetchStart = microtime(true);
foreach (array_chunk($crmIds, self::BATCH_SIZE) as $chunk) {
try {
$companies = $this->client->getCompaniesByIds($chunk, $fields);
foreach ($companies as $companyData) {
$allCompanies[] = $companyData;
}
} catch (\Throwable $e) {
$this->logger->warning('[' . $this->getDisplayName() . '] Batch company fetch failed', [
'chunk_size' => \count($chunk),
'error' => $e->getMessage(),
]);
throw $e;
}
}
$fetchMs = (int) round((microtime(true) - $fetchStart) * 1000);
$fetchedIds = array_map('strval', array_column($allCompanies, 'id'));
$notFoundIds = array_values(array_diff(array_map('strval', $crmIds), $fetchedIds));
if (! empty($notFoundIds)) {
$this->logger->info('[' . $this->getDisplayName() . '] Company CRM IDs not found in HubSpot', [
'teamId' => $this->team->getId(),
'notFoundCount' => \count($notFoundIds),
'notFoundIds' => $notFoundIds,
'requestedCount' => \count($crmIds),
'fetchedCount' => \count($allCompanies),
]);
}
$loopStart = microtime(true);
foreach ($allCompanies as $companyData) {
$accountStart = microtime(true);
try {
$account = $this->importAccount($companyData);
if ($account !== null) {
$result['success_count']++;
}
} catch (\Throwable $e) {
$crmId = $companyData['id'] ?? $companyData['properties']['hs_object_id'] ?? 'unknown';
$result['failed_ids'][] = $crmId;
$result['errors'][$crmId] = $e->getMessage();
$this->logger->warning('[' . $this->getDisplayName() . '] Failed to import account', [
'crmId' => $crmId,
'error' => $e->getMessage(),
]);
}
$accountMs = (int) round((microtime(true) - $accountStart) * 1000);
if ($accountMs > 1000) {
$crmId = $companyData['id'] ?? $companyData['properties']['hs_object_id'] ?? 'unknown';
$slowAccounts[] = ['crmId' => $crmId, 'ms' => $accountMs];
}
}
$loopMs = (int) round((microtime(true) - $loopStart) * 1000);
$totalMs = (int) round((microtime(true) - $batchStart) * 1000);
$this->logger->info('[' . $this->getDisplayName() . '] importAccountBatch timing', [
'teamId' => $this->team->getId(),
'account_count' => \count($allCompanies),
'requested_count' => \count($crmIds),
'not_found_count' => \count($notFoundIds),
'total_ms' => $totalMs,
'fetch_api_ms' => $fetchMs,
'accounts_loop_ms' => $loopMs,
'avg_account_ms' => ! empty($allCompanies) ? (int) round($loopMs / \count($allCompanies)) : 0,
'slow_accounts_count' => \count($slowAccounts),
'slow_accounts' => array_slice($slowAccounts, 0, 10),
]);
return $result;
}
private function getCompanyFields(): array
{
return [
'country',
'name',
'phone',
'domain',
'hs_avatar_filemanager_key',
'industry',
'hubspot_owner_id',
'hs_object_id',
'createdate',
'hs_lastmodifieddate',
];
}
private function importAccount($crmData): ?Account
{
$crmProviderId = $crmData['id'] ?? $crmData['properties']['hs_object_id'] ?? null;
$this->logger->info('[HubSpot] importAccount', [
'crm_provider_id' => $crmProviderId,
'config_id' => $this->config->getId(),
]);
if (empty($crmData['properties'])) {
$this->logger->info('[HubSpot] importAccount failed: empty properties', [
'crm_provider_id' => $crmProviderId,
'config_id' => $this->config->getId(),
]);
return null;
}
$properties = $crmData['properties'];
$crmId = (string) $properties['hs_object_id'];
$countryCode = isset($properties['country']) ? $this->convertCountryNameToCode($properties['country']) : null;
if (isset($properties['phone'])) {
// Trim to our width and attempt to parse it.
$number = mb_strimwidth($properties['phone'], 0, 25);
$parsedNumber = parsePhoneNumber($countryCode, $number);
} else {
$parsedNumber = [];
}
$name = '[unknown]';
if (isset($properties['name'])) {
$name = $properties['name'];
}
$photoPath = $this->prospectPhotoPathService->getOrGeneratePhotoPath(
$this->config,
$crmId,
Account::class,
$crmId,
$name
);
$industry = null;
if (isset($properties['industry'])) {
$industry = mb_strimwidth($properties['industry'], 0, 40);
}
$ownerId = $profile = null;
if (isset($properties['hubspot_owner_id'])) {
$ownerId = $properties['hubspot_owner_id'];
$profile = $this->crmEntityRepository->findProfileByExternalId($this->config, (string) $ownerId);
}
$domain = null;
if (isset($properties['domain'])) {
$domain = StringUtil::resolveDomain($properties['domain']);
}
$remotelyCreatedAt = null;
if (isset($properties['createdate']) && ! empty($properties['createdate'])) {
$remotelyCreatedAt = Carbon::parse($properties['createdate']);
}
$data = [
'crm_provider_id' => $crmId,
'team_id' => $this->team->id,
'user_id' => $profile ? $profile->user_id : null,
'owner_id' => $ownerId,
'name' => mb_strimwidth($name, 0, 191),
'photo_path' => $photoPath,
'industry' => $industry,
'domain' => $domain !== null
? substr($domain, 0, 191)
: null,
'phone' => $parsedNumber['phone'] ?? null,
'ext' => $parsedNumber['ext'] ?? null,
'country_code' => $countryCode,
'remotely_created_at' => $remotelyCreatedAt,
];
return $this->crmEntityRepository->importAccount($this->config, $data);
}
public function deleteContact(string $crmProviderId): bool
{
try {
$contact = $this->crmEntityRepository->findContactByExternalId($this->config, $crmProviderId);
if (! $contact) {
$this->logger->info('[HubSpot] Contact not found for deletion', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
// return success because we do not know which instance is the target
return true;
}
$id = $contact->getId();
$this->logger->info('[HubSpot] Deleting contact via webhook', [
'contact_id' => $id,
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
$contact->delete();
DeleteContactJob::dispatch($id)->afterCommit();
return true;
} catch (Exception $e) {
$this->logger->error('[HubSpot] Failed to delete contact via webhook', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
'error' => $e->getMessage(),
]);
return false;
}
}
public function deleteAccount(string $crmProviderId): bool
{
try {
$account = $this->crmEntityRepository->findAccountByExternalId($this->config, $crmProviderId);
if (! $account) {
$this->logger->info('[HubSpot] Account not found for deletion', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
// return success because we do not know which instance is the target
return true;
}
$id = $account->getId();
$this->logger->info('[HubSpot] Deleting account via webhook', [
'account_id' => $id,
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
$account->delete();
DeleteAccountJob::dispatch($id)->afterCommit();
return true;
} catch (Exception $e) {
$this->logger->error('[HubSpot] Failed to delete account via webhook', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
'error' => $e->getMessage(),
]);
return false;
}
}
public function deleteOpportunity(string $crmProviderId): bool
{
try {
$opportunity = $this->crmEntityRepository->findOpportunityByExternalId($this->config, $crmProviderId);
if (! $opportunity) {
$this->logger->info('[HubSpot] Opportunity not found for deletion', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
// return success because we do not know which instance is the target
return true;
}
$id = $opportunity->getId();
$this->logger->info('[HubSpot] Deleting opportunity via webhook', [
'opportunity_id' => $id,
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
$opportunity->delete();
DeleteOpportunityJob::dispatch($id)->afterCommit();
return true;
} catch (Exception $e) {
$this->logger->error('[HubSpot] Failed to delete opportunity via webhook', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
'error' => $e->getMessage(),
]);
return false;
}
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide
app ~/jiminny/app, folder
.circleci, folder
.cursor, folder
.github
.sonarlint, folder
.vscode, folder
.windsurf, folder
app, sources root
Actions, folder
Component, folder
Acl, folder
ActionItems, folder
Activity, folder
ActivityAnalytics, folder
ActivitySearch, folder
AiActivityType, folder
AiAutomation, folder
AiCallScoring, folder
AskAnything, folder
Dtos, folder
Events, folder
AskAnythingPromptService.php
HistoryService.php
AskJiminnyAi, folder
AWS, folder
BillingManagement, folder
Cache, folder
CoachingFeedback, folder
Country, folder
CustomerApi, folder
Database, folder
Datadog, folder
DateTime, folder
DealInsights, folder
DealRisks, folder
ElasticSearch
Eloquent
Encoding
Encryption
ES
Faker
FeatureFlags
FFMpeg
FileSystem
Gecko
Gong
GuzzleHttp
KeyPoints
Kiosk
LanguageDetection
LiveFeed
Locks
Math
MediaPipeline
MeetingBot, folder
MobileSettings, folder
Model, folder
Notification, folder
Nudge, folder
ParagraphBreaker, folder
ParticipantSpeech, folder
PartitionedCookie, folder
PlaybackPage, folder
Playlist, folder
Prophet, folder
ProphetAi, folder
ProsperWorks, folder
Queue, folder
Job, folder
RateLimitAware.php, abstract class
RateLimitAwareWrapper.php, class
BotsQueueConstants.php
Constants.php
ProcessingQueueConstants.php
Router, folder
Saml2, folder
SCIM, folder
Seeder, folder
Sentry, folder
Serializer, folder
Settings, folder
Sidekick, folder
Slack, folder
TeamInsights, folder
TimeMemoryMapper, folder
Transcription, folder
TranscriptionSummary, folder
Twilio, folder
Uploader, folder
UrlGenerator, folder
Utility, folder
Exceptions, folder
Service, folder
BaseRateLimiter.php, class
EfficientJsonParser.php, class
ProviderRateLimiter.php, class
RateLimiterInstance.php, class
Uuid, folder
Waveform, folder
Webhooks, folder
Workflow, folder
Configuration, folder
Console, folder
Commands, folder
Activities, folder
Analytics, folder
Calendars, folder
Crm, folder
Hubspot, folder
IntegrationApp, folder
Traits, folder
AddLayoutEntities.php, class
AutologDelayedCommand.php, class
BullhornCommandAbstract.php, abstract class
BullhornPingCommand.php, class
BullhornSearchCommand.php, class
BullhornSessionCommand.php, class
CheckActivityLoggableCommand.php, final class
CleanDuplicateFieldDataCommand.php, class
FullSyncOpportunityCommand.php, class
LogActivitiesCommand.php, final class
ManageSyncStrategyCommand.php, class
MatchCrmObjectsCommand.php, class
MatchOpportunityActivitiesCommand.php, class...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"on_screen":true,"help_text":"Git Branch: master","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"Editor for custom.log","depth":4,"on_screen":true,"role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Analyzing…","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits;\n\nuse Carbon\\Carbon;\nuse Exception;\nuse Illuminate\\Support\\Str;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Jobs\\Crm\\Delete\\DeleteAccountJob;\nuse Jiminny\\Jobs\\Crm\\Delete\\DeleteContactJob;\nuse Jiminny\\Jobs\\Crm\\Delete\\DeleteOpportunityJob;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Repositories\\Crm\\CrmEntityRepository;\nuse Jiminny\\Services\\Avatar\\ProspectPhotoPathService;\nuse Jiminny\\Services\\Crm\\Hubspot\\HubspotClientInterface;\nuse Jiminny\\Services\\Crm\\Hubspot\\WebhookSyncBatchProcessor;\nuse Jiminny\\Utils\\StringUtil;\n\ntrait SyncCrmEntitiesTrait\n{\n use OpportunitySyncTrait;\n private const string CDN_URL = 'https://cdn2.hubspot.net/';\n protected CrmEntityRepository $crmEntityRepository;\n protected ProspectPhotoPathService $prospectPhotoPathService;\n\n private function getAssociationDataForCollection(array $collection, string $fromObject, string $toObject): array\n {\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $hsOpportunityIds = array_column($collection, 'id');\n\n return $this->client->getAssociationsData($hsOpportunityIds, $fromObject, $toObject);\n }\n\n private function importAssociationData(array $collection, array $associatedData): array\n {\n $data = [];\n if (! empty($associatedData[$collection['id']])) {\n foreach ($associatedData[$collection['id']] as $id) {\n $data[] = [\n 'id' => $id,\n ];\n }\n }\n\n return ['results' => $data];\n }\n\n /**\n * Sync contacts modified since a given date (manual sync mode).\n *\n * This method fetches contacts from HubSpot API based on modification date and\n * imports them one by one. It is used for:\n * - Manual sync commands (e.g., crm:sync-contact with --from parameter)\n * - Initial sync for new teams\n * - Backfill operations\n *\n * For regular sync webhook batchSyncContacts is used:\n *\n * @param Carbon $since Fetch contacts modified after this date\n * @param Carbon|null $to Optional end date for modification range\n *\n * @return int Number of contacts successfully synced\n */\n public function syncContacts(Carbon $since, ?Carbon $to = null): int\n {\n $syncCount = 0;\n\n try {\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $fields = $this->getContactFields();\n $payload = $this->payloadBuilder->getRecentlyUpdatedSearchPayload($since, $to, $fields);\n\n foreach ($this->client->getPaginatedDataGenerator($payload, 'contacts') as $hsContact) {\n $this->importContact($hsContact);\n $syncCount++;\n }\n } catch (Exception $exception) {\n $this->logger->error('[' . $this->getDisplayName() . '] Sync contacts failed', [\n 'teamId' => $this->team->getUuid(),\n 'reason' => $exception->getMessage(),\n ]);\n }\n\n return $syncCount;\n }\n\n /**\n * @inheritdoc\n */\n public function syncContact(string $crmId): ?Contact\n {\n try {\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $fields = $this->getContactFields();\n $hsContact = $this->client->getContactById($crmId, $fields);\n } catch (\\HubSpot\\Client\\Crm\\Contacts\\ApiException $e) {\n $this->logger->info('[' . $this->getDisplayName() . '] Contact fetch failed', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n return null;\n } catch (CrmException $e) {\n $this->logger->info('[' . $this->getDisplayName() . '] Contact not found', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n return null;\n }\n\n if (empty($hsContact['properties']) || empty($hsContact['id'])) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Contact data incomplete', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'has_properties' => ! empty($hsContact['properties']),\n 'has_id' => ! empty($hsContact['id']),\n ]);\n\n return null;\n }\n\n return $this->importContact($hsContact);\n }\n\n private function getContactFields(): array\n {\n return [\n 'associatedcompanyid',\n 'country',\n 'firstname',\n 'lastname',\n 'phone',\n 'mobilephone',\n 'email',\n 'photo',\n 'hs_avatar_filemanager_key',\n 'jobtitle',\n 'hubspot_owner_id',\n 'hs_object_id',\n 'createdate',\n 'hs_lastmodifieddate',\n ];\n }\n\n /**\n * @inheritdoc\n */\n private function importContact($crmData, array $accountMappings = []): ?Contact\n {\n $crmProviderId = $crmData['id'] ?? null;\n\n $this->logger->info('[HubSpot] importContact', [\n 'crm_provider_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n if (empty($crmData['properties'])) {\n $this->logger->info('[HubSpot] importContact failed: empty properties', [\n 'crm_provider_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n return null;\n }\n\n $properties = $crmData['properties'];\n $crmId = (string) $crmData['id'];\n\n $accountId = $this->resolveContactAccount($properties, $accountMappings);\n $data = $this->buildContactData($crmId, $properties, $accountId);\n\n return $this->crmEntityRepository->importContact($this->config, $data);\n }\n\n private function resolveContactAccount(array $properties, array $accountMappings): ?int\n {\n if (empty($properties['associatedcompanyid'])) {\n return null;\n }\n\n $companyId = (string) $properties['associatedcompanyid'];\n\n if (! empty($accountMappings)) {\n return $accountMappings[$companyId] ?? null;\n }\n\n return $this->crmEntityRepository->findAccountByExternalId(\n $this->team->getCrmConfiguration(),\n $companyId\n )?->getId() ?? $this->syncAccount($companyId)?->getId();\n }\n\n private function buildContactData(string $crmId, array $properties, ?int $accountId): array\n {\n $countryCode = $this->buildContactCountry($properties);\n $name = $this->buildContactName($properties);\n $photoPath = $this->teamService->generateAvatar(\n $crmId,\n empty($name) ? ($properties['email'] ?? 'N/A') : $name,\n );\n $parsedNumber = $this->buildContactPhone($countryCode, $properties);\n $mobileNumber = $this->buildContactMobilePhone($countryCode, $properties);\n\n $ownerId = $properties['hubspot_owner_id'] ?? null;\n $profile = $ownerId !== null\n ? $this->crmEntityRepository->findProfileByExternalId($this->config, (string) $ownerId)\n : null;\n\n $ext = (isset($parsedNumber['ext']) && is_string($parsedNumber['ext']) && strlen($parsedNumber['ext']) <= 10)\n ? $parsedNumber['ext']\n : null;\n\n $title = isset($properties['jobtitle']) ? mb_strimwidth($properties['jobtitle'], 0, 128) : null;\n $email = isset($properties['email']) ? mb_strimwidth($properties['email'], 0, 191) : null;\n $remotelyCreatedAt = ! empty($properties['createdate']) ? Carbon::parse($properties['createdate']) : null;\n\n return [\n 'crm_provider_id' => $crmId,\n 'team_id' => $this->team->getId(),\n 'account_id' => $accountId,\n 'user_id' => $profile?->getUserId(),\n 'owner_id' => $ownerId,\n 'name' => $name,\n 'title' => $title,\n 'email' => $email,\n 'country_code' => $countryCode,\n 'phone' => $parsedNumber['phone'] ?? null,\n 'mobile_phone' => $mobileNumber ?? null,\n 'ext' => $ext,\n 'photo_path' => $photoPath,\n 'remotely_created_at' => $remotelyCreatedAt,\n ];\n }\n\n /**\n * @param $properties\n */\n private function buildContactName($properties): string\n {\n if (is_array($properties)) {\n return $this->buildContactNameFromArray($properties);\n }\n\n return $this->buildContactNameFromObject($properties);\n }\n\n private function buildContactNameFromArray(array $properties): string\n {\n if (! empty($properties['name'])) {\n return mb_strimwidth($properties['name'], 0, 100);\n }\n\n $name = '';\n if (! empty($properties['firstname'])) {\n $name = $properties['firstname'] . ' ';\n }\n\n if (! empty($properties['lastname'])) {\n $name .= $properties['lastname'];\n }\n\n if ($name === '' && ! empty($properties['email'])) {\n $name = $properties['email'];\n }\n\n return mb_strimwidth($name, 0, 100);\n }\n\n private function buildContactNameFromObject($properties): string\n {\n $name = '';\n if (isset($properties->firstname)) {\n $name = $properties->firstname->value . ' ';\n }\n if (isset($properties->lastname)) {\n $name .= $properties->lastname->value;\n }\n if ($name === '' && isset($properties->email)) {\n $name = $properties->email->value;\n }\n\n return mb_strimwidth($name, 0, 100);\n }\n\n /**\n * @param $properties\n */\n private function buildContactPhone(?string $countryCode, $properties): ?array\n {\n if (is_array($properties) && empty($properties['phone']) === false) {\n $number = mb_strimwidth($properties['phone'], 0, 25);\n\n return parsePhoneNumber($countryCode, $number);\n } elseif (isset($properties->phone)) {\n $number = mb_strimwidth($properties->phone->value, 0, 25);\n\n return parsePhoneNumber($countryCode, $number);\n }\n\n return [];\n }\n\n /**\n * @param $properties\n */\n private function buildContactMobilePhone(?string $countryCode, $properties): ?string\n {\n return isset($properties['mobilephone'])\n ? Str::limit(phone_e164($countryCode, $properties['mobilephone']), 25, '')\n : null;\n }\n\n /**\n * @param $properties\n * @param $account\n */\n private function buildContactCountry($properties): ?string\n {\n if (is_array($properties) && empty($properties['country']) === false) {\n return $this->convertCountryNameToCode($properties['country']);\n }\n\n if (isset($properties->country)) {\n return $this->convertCountryNameToCode($properties->country->value);\n }\n\n return null;\n }\n\n /**\n * HubSpot doesn't have leads, so this method does nothing.\n *\n * @param Carbon $since\n * @param Carbon|null $to\n * @param string|null $crmProfileId\n *\n * @return int\n */\n public function syncLeads(Carbon $since, ?Carbon $to = null, ?string $crmProfileId = null): int\n {\n // Mark unused parameters to avoid code smell warnings\n unset($since, $to, $crmProfileId);\n\n return 0;\n }\n\n /**\n * HubSpot doesn't have leads.\n *\n * @param string $crmId\n *\n * @inheritdoc\n */\n public function syncLead(string $crmId): ?Lead\n {\n // Mark unused parameter to avoid code smell warnings\n unset($crmId);\n\n return null;\n }\n\n /**\n * Sync accounts (companies) modified since a given date (manual sync mode).\n *\n * This method fetches companies from HubSpot API based on modification date and\n * imports them one by one. It is used for:\n * - Manual sync commands (e.g., crm:sync-account with --from parameter)\n * - Initial sync for new teams\n * - Backfill operations\n *\n * For regular sync webhook batchSyncCompanies is used:\n *\n * @param Carbon $since Fetch companies modified after this date\n * @param Carbon|null $to Optional end date for modification range\n *\n * @return int Number of companies successfully synced\n */\n public function syncAccounts(Carbon $since, ?Carbon $to = null): int\n {\n $syncCount = 0;\n\n try {\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $fields = $this->getCompanyFields();\n $payload = $this->payloadBuilder->getRecentlyUpdatedSearchPayload($since, $to, $fields);\n\n foreach ($this->client->getPaginatedDataGenerator($payload, 'companies') as $hsAccount) {\n $this->importAccount($hsAccount);\n $syncCount++;\n }\n } catch (Exception $exception) {\n $this->logger->error('[' . $this->getDisplayName() . '] Sync accounts failed', [\n 'teamId' => $this->team->getUuid(),\n 'reason' => $exception->getMessage(),\n ]);\n }\n\n return $syncCount;\n }\n\n /**\n * @inheritdoc\n */\n public function syncAccount(string $crmId): ?Account\n {\n try {\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $fields = $this->getCompanyFields();\n $hsAccount = $this->client->getAccountById($crmId, $fields);\n } catch (\\HubSpot\\Client\\Crm\\Companies\\ApiException $e) {\n $this->logger->info('[' . $this->getDisplayName() . '] Account fetch failed', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n return null;\n } catch (CrmException $e) {\n $this->logger->info('[' . $this->getDisplayName() . '] Account not found', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n return null;\n }\n\n return $this->importAccount($hsAccount);\n }\n\n /**\n * Process webhook-collected contact batches.\n *\n * Drains Redis sets containing contact CRM IDs collected from webhook events\n * and dispatches ImportContactBatch jobs for batch processing.\n *\n * @return int Number of contact IDs dispatched to jobs\n */\n public function batchSyncContacts(): int\n {\n $configId = $this->team->getCrmConfiguration()->getId();\n\n return $this->batchProcessor->processBatchesForObjectType(\n WebhookSyncBatchProcessor::OBJECT_TYPE_CONTACT,\n $configId\n );\n }\n\n public function importContactBatchByIds(array $crmIds): array\n {\n $result = [\n 'success_count' => 0,\n 'failed_ids' => [],\n 'errors' => [],\n ];\n\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $batchStart = microtime(true);\n $slowContacts = [];\n\n $fetchStart = microtime(true);\n $allContacts = $this->fetchContactsByIdsInChunks($crmIds);\n $fetchMs = (int) round((microtime(true) - $fetchStart) * 1000);\n\n $fetchedIds = array_map('strval', array_column($allContacts, 'id'));\n $notFoundIds = array_values(array_diff(array_map('strval', $crmIds), $fetchedIds));\n\n if (! empty($notFoundIds)) {\n $this->logger->info('[' . $this->getDisplayName() . '] Contact CRM IDs not found in HubSpot', [\n 'teamId' => $this->team->getId(),\n 'notFoundCount' => \\count($notFoundIds),\n 'notFoundIds' => $notFoundIds,\n 'requestedCount' => \\count($crmIds),\n 'fetchedCount' => \\count($allContacts),\n ]);\n }\n\n if (empty($allContacts)) {\n return $result;\n }\n\n $prepareStart = microtime(true);\n $accountMappings = $this->prepareAccountMappingsForContacts($allContacts);\n $prepareAccountsMs = (int) round((microtime(true) - $prepareStart) * 1000);\n\n $loopStart = microtime(true);\n foreach ($allContacts as $contactData) {\n $contactStart = microtime(true);\n\n try {\n $contact = $this->importContact($contactData, $accountMappings);\n if ($contact !== null) {\n $result['success_count']++;\n }\n } catch (\\Throwable $e) {\n $crmId = $contactData['id'] ?? 'unknown';\n $result['failed_ids'][] = $crmId;\n $result['errors'][$crmId] = $e->getMessage();\n\n $this->logger->warning('[' . $this->getDisplayName() . '] Failed to import contact', [\n 'teamId' => $this->team->getId(),\n 'crmId' => $crmId,\n 'error' => $e->getMessage(),\n ]);\n }\n\n $contactMs = (int) round((microtime(true) - $contactStart) * 1000);\n if ($contactMs > 1000) {\n $slowContacts[] = ['crmId' => $contactData['id'] ?? 'unknown', 'ms' => $contactMs];\n }\n }\n $loopMs = (int) round((microtime(true) - $loopStart) * 1000);\n $totalMs = (int) round((microtime(true) - $batchStart) * 1000);\n\n $this->logger->info('[' . $this->getDisplayName() . '] importContactBatch timing', [\n 'teamId' => $this->team->getId(),\n 'contact_count' => \\count($allContacts),\n 'requested_count' => \\count($crmIds),\n 'not_found_count' => \\count($notFoundIds),\n 'total_ms' => $totalMs,\n 'fetch_api_ms' => $fetchMs,\n 'prepare_accounts_ms' => $prepareAccountsMs,\n 'contacts_loop_ms' => $loopMs,\n 'avg_contact_ms' => ! empty($allContacts) ? (int) round($loopMs / \\count($allContacts)) : 0,\n 'slow_contacts_count' => \\count($slowContacts),\n 'slow_contacts' => array_slice($slowContacts, 0, 10),\n ]);\n\n return $result;\n }\n\n private function fetchContactsByIdsInChunks(array $crmIds): array\n {\n $fields = $this->getContactFields();\n $allContacts = [];\n\n foreach (array_chunk($crmIds, self::BATCH_SIZE) as $chunk) {\n try {\n $contacts = $this->client->getContactsByIds($chunk, $fields);\n foreach ($contacts as $contactData) {\n $allContacts[] = $contactData;\n }\n } catch (\\Throwable $e) {\n // @TODO what will happen if this exception is thrown\n $this->logger->warning('[' . $this->getDisplayName() . '] Batch contact fetch failed', [\n 'chunk_size' => \\count($chunk),\n 'error' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n }\n\n return $allContacts;\n }\n\n private function prepareAccountMappingsForContacts(array $contacts): array\n {\n $companyIds = [];\n foreach ($contacts as $contact) {\n $companyId = $contact['properties']['associatedcompanyid'] ?? null;\n if ($companyId !== null && $companyId !== '') {\n $companyIds[] = (string) $companyId;\n }\n }\n\n $companyIds = array_unique($companyIds);\n\n if (empty($companyIds)) {\n return [];\n }\n\n $mappings = $this->crmEntityRepository\n ->getExistingAccountIdsMap($this->config, $companyIds);\n\n $missingCompanyIds = array_diff($companyIds, array_keys($mappings));\n\n if (empty($missingCompanyIds)) {\n return $mappings;\n }\n\n $this->logger->info('[' . $this->getDisplayName() . '] Batch syncing missing accounts for contacts', [\n 'teamId' => $this->team->getId(),\n 'total_companies' => \\count($companyIds),\n 'existing_companies' => \\count($mappings),\n 'missing_companies' => \\count($missingCompanyIds),\n ]);\n\n try {\n $syncedAccounts = $this->batchSyncAccountsForContacts($missingCompanyIds);\n $mappings = array_merge($mappings, $syncedAccounts);\n } catch (\\Throwable $e) {\n $this->logger->error('[' . $this->getDisplayName() . '] Failed to batch sync missing accounts', [\n 'teamId' => $this->team->getId(),\n 'missingCompanyIds' => $missingCompanyIds,\n 'missingCount' => count($missingCompanyIds),\n 'error' => $e->getMessage(),\n ]);\n }\n\n return $mappings;\n }\n\n private function batchSyncAccountsForContacts(array $companyIds): array\n {\n $syncedAccounts = [];\n $fields = $this->getCompanyFields();\n\n foreach (array_chunk($companyIds, self::BATCH_SIZE) as $chunk) {\n try {\n $companies = $this->client->getCompaniesByIds($chunk, $fields);\n\n foreach ($companies as $companyData) {\n try {\n $account = $this->importAccount($companyData);\n if ($account) {\n $syncedAccounts[$account->getCrmProviderId()] = $account->getId();\n }\n } catch (\\Throwable $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Failed to import account in batch', [\n 'teamId' => $this->team->getId(),\n 'companyId' => $companyData['id'] ?? 'unknown',\n 'error' => $e->getMessage(),\n ]);\n }\n }\n } catch (\\Throwable $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Batch company fetch failed', [\n 'teamId' => $this->team->getId(),\n 'chunk_size' => \\count($chunk),\n 'error' => $e->getMessage(),\n ]);\n }\n }\n\n return $syncedAccounts;\n }\n\n /**\n * Process webhook-collected company batches.\n *\n * Drains Redis sets containing company CRM IDs collected from webhook events\n * and dispatches ImportAccountBatch jobs for batch processing.\n *\n * @return int Number of company IDs dispatched to jobs\n */\n public function batchSyncCompanies(): int\n {\n $configId = $this->team->getCrmConfiguration()->getId();\n\n return $this->batchProcessor->processBatchesForObjectType(\n WebhookSyncBatchProcessor::OBJECT_TYPE_COMPANY,\n $configId\n );\n }\n\n public function importAccountBatchByIds(array $crmIds): array\n {\n $result = [\n 'success_count' => 0,\n 'failed_ids' => [],\n 'errors' => [],\n ];\n\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $batchStart = microtime(true);\n $slowAccounts = [];\n\n $fields = $this->getCompanyFields();\n $allCompanies = [];\n\n $fetchStart = microtime(true);\n foreach (array_chunk($crmIds, self::BATCH_SIZE) as $chunk) {\n try {\n $companies = $this->client->getCompaniesByIds($chunk, $fields);\n foreach ($companies as $companyData) {\n $allCompanies[] = $companyData;\n }\n } catch (\\Throwable $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Batch company fetch failed', [\n 'chunk_size' => \\count($chunk),\n 'error' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n }\n $fetchMs = (int) round((microtime(true) - $fetchStart) * 1000);\n\n $fetchedIds = array_map('strval', array_column($allCompanies, 'id'));\n $notFoundIds = array_values(array_diff(array_map('strval', $crmIds), $fetchedIds));\n\n if (! empty($notFoundIds)) {\n $this->logger->info('[' . $this->getDisplayName() . '] Company CRM IDs not found in HubSpot', [\n 'teamId' => $this->team->getId(),\n 'notFoundCount' => \\count($notFoundIds),\n 'notFoundIds' => $notFoundIds,\n 'requestedCount' => \\count($crmIds),\n 'fetchedCount' => \\count($allCompanies),\n ]);\n }\n\n $loopStart = microtime(true);\n foreach ($allCompanies as $companyData) {\n $accountStart = microtime(true);\n\n try {\n $account = $this->importAccount($companyData);\n if ($account !== null) {\n $result['success_count']++;\n }\n } catch (\\Throwable $e) {\n $crmId = $companyData['id'] ?? $companyData['properties']['hs_object_id'] ?? 'unknown';\n $result['failed_ids'][] = $crmId;\n $result['errors'][$crmId] = $e->getMessage();\n\n $this->logger->warning('[' . $this->getDisplayName() . '] Failed to import account', [\n 'crmId' => $crmId,\n 'error' => $e->getMessage(),\n ]);\n }\n\n $accountMs = (int) round((microtime(true) - $accountStart) * 1000);\n if ($accountMs > 1000) {\n $crmId = $companyData['id'] ?? $companyData['properties']['hs_object_id'] ?? 'unknown';\n $slowAccounts[] = ['crmId' => $crmId, 'ms' => $accountMs];\n }\n }\n $loopMs = (int) round((microtime(true) - $loopStart) * 1000);\n $totalMs = (int) round((microtime(true) - $batchStart) * 1000);\n\n $this->logger->info('[' . $this->getDisplayName() . '] importAccountBatch timing', [\n 'teamId' => $this->team->getId(),\n 'account_count' => \\count($allCompanies),\n 'requested_count' => \\count($crmIds),\n 'not_found_count' => \\count($notFoundIds),\n 'total_ms' => $totalMs,\n 'fetch_api_ms' => $fetchMs,\n 'accounts_loop_ms' => $loopMs,\n 'avg_account_ms' => ! empty($allCompanies) ? (int) round($loopMs / \\count($allCompanies)) : 0,\n 'slow_accounts_count' => \\count($slowAccounts),\n 'slow_accounts' => array_slice($slowAccounts, 0, 10),\n ]);\n\n return $result;\n }\n\n private function getCompanyFields(): array\n {\n return [\n 'country',\n 'name',\n 'phone',\n 'domain',\n 'hs_avatar_filemanager_key',\n 'industry',\n 'hubspot_owner_id',\n 'hs_object_id',\n 'createdate',\n 'hs_lastmodifieddate',\n ];\n }\n\n private function importAccount($crmData): ?Account\n {\n $crmProviderId = $crmData['id'] ?? $crmData['properties']['hs_object_id'] ?? null;\n\n $this->logger->info('[HubSpot] importAccount', [\n 'crm_provider_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n if (empty($crmData['properties'])) {\n $this->logger->info('[HubSpot] importAccount failed: empty properties', [\n 'crm_provider_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n return null;\n }\n\n $properties = $crmData['properties'];\n $crmId = (string) $properties['hs_object_id'];\n\n $countryCode = isset($properties['country']) ? $this->convertCountryNameToCode($properties['country']) : null;\n\n if (isset($properties['phone'])) {\n // Trim to our width and attempt to parse it.\n $number = mb_strimwidth($properties['phone'], 0, 25);\n $parsedNumber = parsePhoneNumber($countryCode, $number);\n } else {\n $parsedNumber = [];\n }\n\n $name = '[unknown]';\n if (isset($properties['name'])) {\n $name = $properties['name'];\n }\n\n $photoPath = $this->prospectPhotoPathService->getOrGeneratePhotoPath(\n $this->config,\n $crmId,\n Account::class,\n $crmId,\n $name\n );\n\n $industry = null;\n if (isset($properties['industry'])) {\n $industry = mb_strimwidth($properties['industry'], 0, 40);\n }\n\n $ownerId = $profile = null;\n if (isset($properties['hubspot_owner_id'])) {\n $ownerId = $properties['hubspot_owner_id'];\n $profile = $this->crmEntityRepository->findProfileByExternalId($this->config, (string) $ownerId);\n }\n\n $domain = null;\n if (isset($properties['domain'])) {\n $domain = StringUtil::resolveDomain($properties['domain']);\n }\n\n $remotelyCreatedAt = null;\n if (isset($properties['createdate']) && ! empty($properties['createdate'])) {\n $remotelyCreatedAt = Carbon::parse($properties['createdate']);\n }\n\n $data = [\n 'crm_provider_id' => $crmId,\n 'team_id' => $this->team->id,\n 'user_id' => $profile ? $profile->user_id : null,\n 'owner_id' => $ownerId,\n 'name' => mb_strimwidth($name, 0, 191),\n 'photo_path' => $photoPath,\n 'industry' => $industry,\n 'domain' => $domain !== null\n ? substr($domain, 0, 191)\n : null,\n 'phone' => $parsedNumber['phone'] ?? null,\n 'ext' => $parsedNumber['ext'] ?? null,\n 'country_code' => $countryCode,\n 'remotely_created_at' => $remotelyCreatedAt,\n ];\n\n return $this->crmEntityRepository->importAccount($this->config, $data);\n }\n\n public function deleteContact(string $crmProviderId): bool\n {\n try {\n $contact = $this->crmEntityRepository->findContactByExternalId($this->config, $crmProviderId);\n\n if (! $contact) {\n $this->logger->info('[HubSpot] Contact not found for deletion', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n // return success because we do not know which instance is the target\n return true;\n }\n\n $id = $contact->getId();\n\n $this->logger->info('[HubSpot] Deleting contact via webhook', [\n 'contact_id' => $id,\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n $contact->delete();\n DeleteContactJob::dispatch($id)->afterCommit();\n\n return true;\n } catch (Exception $e) {\n $this->logger->error('[HubSpot] Failed to delete contact via webhook', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n return false;\n }\n }\n\n public function deleteAccount(string $crmProviderId): bool\n {\n try {\n $account = $this->crmEntityRepository->findAccountByExternalId($this->config, $crmProviderId);\n\n if (! $account) {\n $this->logger->info('[HubSpot] Account not found for deletion', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n // return success because we do not know which instance is the target\n return true;\n }\n\n $id = $account->getId();\n\n $this->logger->info('[HubSpot] Deleting account via webhook', [\n 'account_id' => $id,\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n $account->delete();\n DeleteAccountJob::dispatch($id)->afterCommit();\n\n return true;\n } catch (Exception $e) {\n $this->logger->error('[HubSpot] Failed to delete account via webhook', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n return false;\n }\n }\n\n public function deleteOpportunity(string $crmProviderId): bool\n {\n try {\n $opportunity = $this->crmEntityRepository->findOpportunityByExternalId($this->config, $crmProviderId);\n\n if (! $opportunity) {\n $this->logger->info('[HubSpot] Opportunity not found for deletion', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n // return success because we do not know which instance is the target\n return true;\n }\n\n $id = $opportunity->getId();\n\n $this->logger->info('[HubSpot] Deleting opportunity via webhook', [\n 'opportunity_id' => $id,\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n $opportunity->delete();\n DeleteOpportunityJob::dispatch($id)->afterCommit();\n\n return true;\n } catch (Exception $e) {\n $this->logger->error('[HubSpot] Failed to delete opportunity via webhook', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n return false;\n }\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits;\n\nuse Carbon\\Carbon;\nuse Exception;\nuse Illuminate\\Support\\Str;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Jobs\\Crm\\Delete\\DeleteAccountJob;\nuse Jiminny\\Jobs\\Crm\\Delete\\DeleteContactJob;\nuse Jiminny\\Jobs\\Crm\\Delete\\DeleteOpportunityJob;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Repositories\\Crm\\CrmEntityRepository;\nuse Jiminny\\Services\\Avatar\\ProspectPhotoPathService;\nuse Jiminny\\Services\\Crm\\Hubspot\\HubspotClientInterface;\nuse Jiminny\\Services\\Crm\\Hubspot\\WebhookSyncBatchProcessor;\nuse Jiminny\\Utils\\StringUtil;\n\ntrait SyncCrmEntitiesTrait\n{\n use OpportunitySyncTrait;\n private const string CDN_URL = 'https://cdn2.hubspot.net/';\n protected CrmEntityRepository $crmEntityRepository;\n protected ProspectPhotoPathService $prospectPhotoPathService;\n\n private function getAssociationDataForCollection(array $collection, string $fromObject, string $toObject): array\n {\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $hsOpportunityIds = array_column($collection, 'id');\n\n return $this->client->getAssociationsData($hsOpportunityIds, $fromObject, $toObject);\n }\n\n private function importAssociationData(array $collection, array $associatedData): array\n {\n $data = [];\n if (! empty($associatedData[$collection['id']])) {\n foreach ($associatedData[$collection['id']] as $id) {\n $data[] = [\n 'id' => $id,\n ];\n }\n }\n\n return ['results' => $data];\n }\n\n /**\n * Sync contacts modified since a given date (manual sync mode).\n *\n * This method fetches contacts from HubSpot API based on modification date and\n * imports them one by one. It is used for:\n * - Manual sync commands (e.g., crm:sync-contact with --from parameter)\n * - Initial sync for new teams\n * - Backfill operations\n *\n * For regular sync webhook batchSyncContacts is used:\n *\n * @param Carbon $since Fetch contacts modified after this date\n * @param Carbon|null $to Optional end date for modification range\n *\n * @return int Number of contacts successfully synced\n */\n public function syncContacts(Carbon $since, ?Carbon $to = null): int\n {\n $syncCount = 0;\n\n try {\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $fields = $this->getContactFields();\n $payload = $this->payloadBuilder->getRecentlyUpdatedSearchPayload($since, $to, $fields);\n\n foreach ($this->client->getPaginatedDataGenerator($payload, 'contacts') as $hsContact) {\n $this->importContact($hsContact);\n $syncCount++;\n }\n } catch (Exception $exception) {\n $this->logger->error('[' . $this->getDisplayName() . '] Sync contacts failed', [\n 'teamId' => $this->team->getUuid(),\n 'reason' => $exception->getMessage(),\n ]);\n }\n\n return $syncCount;\n }\n\n /**\n * @inheritdoc\n */\n public function syncContact(string $crmId): ?Contact\n {\n try {\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $fields = $this->getContactFields();\n $hsContact = $this->client->getContactById($crmId, $fields);\n } catch (\\HubSpot\\Client\\Crm\\Contacts\\ApiException $e) {\n $this->logger->info('[' . $this->getDisplayName() . '] Contact fetch failed', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n return null;\n } catch (CrmException $e) {\n $this->logger->info('[' . $this->getDisplayName() . '] Contact not found', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n return null;\n }\n\n if (empty($hsContact['properties']) || empty($hsContact['id'])) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Contact data incomplete', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'has_properties' => ! empty($hsContact['properties']),\n 'has_id' => ! empty($hsContact['id']),\n ]);\n\n return null;\n }\n\n return $this->importContact($hsContact);\n }\n\n private function getContactFields(): array\n {\n return [\n 'associatedcompanyid',\n 'country',\n 'firstname',\n 'lastname',\n 'phone',\n 'mobilephone',\n 'email',\n 'photo',\n 'hs_avatar_filemanager_key',\n 'jobtitle',\n 'hubspot_owner_id',\n 'hs_object_id',\n 'createdate',\n 'hs_lastmodifieddate',\n ];\n }\n\n /**\n * @inheritdoc\n */\n private function importContact($crmData, array $accountMappings = []): ?Contact\n {\n $crmProviderId = $crmData['id'] ?? null;\n\n $this->logger->info('[HubSpot] importContact', [\n 'crm_provider_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n if (empty($crmData['properties'])) {\n $this->logger->info('[HubSpot] importContact failed: empty properties', [\n 'crm_provider_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n return null;\n }\n\n $properties = $crmData['properties'];\n $crmId = (string) $crmData['id'];\n\n $accountId = $this->resolveContactAccount($properties, $accountMappings);\n $data = $this->buildContactData($crmId, $properties, $accountId);\n\n return $this->crmEntityRepository->importContact($this->config, $data);\n }\n\n private function resolveContactAccount(array $properties, array $accountMappings): ?int\n {\n if (empty($properties['associatedcompanyid'])) {\n return null;\n }\n\n $companyId = (string) $properties['associatedcompanyid'];\n\n if (! empty($accountMappings)) {\n return $accountMappings[$companyId] ?? null;\n }\n\n return $this->crmEntityRepository->findAccountByExternalId(\n $this->team->getCrmConfiguration(),\n $companyId\n )?->getId() ?? $this->syncAccount($companyId)?->getId();\n }\n\n private function buildContactData(string $crmId, array $properties, ?int $accountId): array\n {\n $countryCode = $this->buildContactCountry($properties);\n $name = $this->buildContactName($properties);\n $photoPath = $this->teamService->generateAvatar(\n $crmId,\n empty($name) ? ($properties['email'] ?? 'N/A') : $name,\n );\n $parsedNumber = $this->buildContactPhone($countryCode, $properties);\n $mobileNumber = $this->buildContactMobilePhone($countryCode, $properties);\n\n $ownerId = $properties['hubspot_owner_id'] ?? null;\n $profile = $ownerId !== null\n ? $this->crmEntityRepository->findProfileByExternalId($this->config, (string) $ownerId)\n : null;\n\n $ext = (isset($parsedNumber['ext']) && is_string($parsedNumber['ext']) && strlen($parsedNumber['ext']) <= 10)\n ? $parsedNumber['ext']\n : null;\n\n $title = isset($properties['jobtitle']) ? mb_strimwidth($properties['jobtitle'], 0, 128) : null;\n $email = isset($properties['email']) ? mb_strimwidth($properties['email'], 0, 191) : null;\n $remotelyCreatedAt = ! empty($properties['createdate']) ? Carbon::parse($properties['createdate']) : null;\n\n return [\n 'crm_provider_id' => $crmId,\n 'team_id' => $this->team->getId(),\n 'account_id' => $accountId,\n 'user_id' => $profile?->getUserId(),\n 'owner_id' => $ownerId,\n 'name' => $name,\n 'title' => $title,\n 'email' => $email,\n 'country_code' => $countryCode,\n 'phone' => $parsedNumber['phone'] ?? null,\n 'mobile_phone' => $mobileNumber ?? null,\n 'ext' => $ext,\n 'photo_path' => $photoPath,\n 'remotely_created_at' => $remotelyCreatedAt,\n ];\n }\n\n /**\n * @param $properties\n */\n private function buildContactName($properties): string\n {\n if (is_array($properties)) {\n return $this->buildContactNameFromArray($properties);\n }\n\n return $this->buildContactNameFromObject($properties);\n }\n\n private function buildContactNameFromArray(array $properties): string\n {\n if (! empty($properties['name'])) {\n return mb_strimwidth($properties['name'], 0, 100);\n }\n\n $name = '';\n if (! empty($properties['firstname'])) {\n $name = $properties['firstname'] . ' ';\n }\n\n if (! empty($properties['lastname'])) {\n $name .= $properties['lastname'];\n }\n\n if ($name === '' && ! empty($properties['email'])) {\n $name = $properties['email'];\n }\n\n return mb_strimwidth($name, 0, 100);\n }\n\n private function buildContactNameFromObject($properties): string\n {\n $name = '';\n if (isset($properties->firstname)) {\n $name = $properties->firstname->value . ' ';\n }\n if (isset($properties->lastname)) {\n $name .= $properties->lastname->value;\n }\n if ($name === '' && isset($properties->email)) {\n $name = $properties->email->value;\n }\n\n return mb_strimwidth($name, 0, 100);\n }\n\n /**\n * @param $properties\n */\n private function buildContactPhone(?string $countryCode, $properties): ?array\n {\n if (is_array($properties) && empty($properties['phone']) === false) {\n $number = mb_strimwidth($properties['phone'], 0, 25);\n\n return parsePhoneNumber($countryCode, $number);\n } elseif (isset($properties->phone)) {\n $number = mb_strimwidth($properties->phone->value, 0, 25);\n\n return parsePhoneNumber($countryCode, $number);\n }\n\n return [];\n }\n\n /**\n * @param $properties\n */\n private function buildContactMobilePhone(?string $countryCode, $properties): ?string\n {\n return isset($properties['mobilephone'])\n ? Str::limit(phone_e164($countryCode, $properties['mobilephone']), 25, '')\n : null;\n }\n\n /**\n * @param $properties\n * @param $account\n */\n private function buildContactCountry($properties): ?string\n {\n if (is_array($properties) && empty($properties['country']) === false) {\n return $this->convertCountryNameToCode($properties['country']);\n }\n\n if (isset($properties->country)) {\n return $this->convertCountryNameToCode($properties->country->value);\n }\n\n return null;\n }\n\n /**\n * HubSpot doesn't have leads, so this method does nothing.\n *\n * @param Carbon $since\n * @param Carbon|null $to\n * @param string|null $crmProfileId\n *\n * @return int\n */\n public function syncLeads(Carbon $since, ?Carbon $to = null, ?string $crmProfileId = null): int\n {\n // Mark unused parameters to avoid code smell warnings\n unset($since, $to, $crmProfileId);\n\n return 0;\n }\n\n /**\n * HubSpot doesn't have leads.\n *\n * @param string $crmId\n *\n * @inheritdoc\n */\n public function syncLead(string $crmId): ?Lead\n {\n // Mark unused parameter to avoid code smell warnings\n unset($crmId);\n\n return null;\n }\n\n /**\n * Sync accounts (companies) modified since a given date (manual sync mode).\n *\n * This method fetches companies from HubSpot API based on modification date and\n * imports them one by one. It is used for:\n * - Manual sync commands (e.g., crm:sync-account with --from parameter)\n * - Initial sync for new teams\n * - Backfill operations\n *\n * For regular sync webhook batchSyncCompanies is used:\n *\n * @param Carbon $since Fetch companies modified after this date\n * @param Carbon|null $to Optional end date for modification range\n *\n * @return int Number of companies successfully synced\n */\n public function syncAccounts(Carbon $since, ?Carbon $to = null): int\n {\n $syncCount = 0;\n\n try {\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $fields = $this->getCompanyFields();\n $payload = $this->payloadBuilder->getRecentlyUpdatedSearchPayload($since, $to, $fields);\n\n foreach ($this->client->getPaginatedDataGenerator($payload, 'companies') as $hsAccount) {\n $this->importAccount($hsAccount);\n $syncCount++;\n }\n } catch (Exception $exception) {\n $this->logger->error('[' . $this->getDisplayName() . '] Sync accounts failed', [\n 'teamId' => $this->team->getUuid(),\n 'reason' => $exception->getMessage(),\n ]);\n }\n\n return $syncCount;\n }\n\n /**\n * @inheritdoc\n */\n public function syncAccount(string $crmId): ?Account\n {\n try {\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $fields = $this->getCompanyFields();\n $hsAccount = $this->client->getAccountById($crmId, $fields);\n } catch (\\HubSpot\\Client\\Crm\\Companies\\ApiException $e) {\n $this->logger->info('[' . $this->getDisplayName() . '] Account fetch failed', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n return null;\n } catch (CrmException $e) {\n $this->logger->info('[' . $this->getDisplayName() . '] Account not found', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n return null;\n }\n\n return $this->importAccount($hsAccount);\n }\n\n /**\n * Process webhook-collected contact batches.\n *\n * Drains Redis sets containing contact CRM IDs collected from webhook events\n * and dispatches ImportContactBatch jobs for batch processing.\n *\n * @return int Number of contact IDs dispatched to jobs\n */\n public function batchSyncContacts(): int\n {\n $configId = $this->team->getCrmConfiguration()->getId();\n\n return $this->batchProcessor->processBatchesForObjectType(\n WebhookSyncBatchProcessor::OBJECT_TYPE_CONTACT,\n $configId\n );\n }\n\n public function importContactBatchByIds(array $crmIds): array\n {\n $result = [\n 'success_count' => 0,\n 'failed_ids' => [],\n 'errors' => [],\n ];\n\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $batchStart = microtime(true);\n $slowContacts = [];\n\n $fetchStart = microtime(true);\n $allContacts = $this->fetchContactsByIdsInChunks($crmIds);\n $fetchMs = (int) round((microtime(true) - $fetchStart) * 1000);\n\n $fetchedIds = array_map('strval', array_column($allContacts, 'id'));\n $notFoundIds = array_values(array_diff(array_map('strval', $crmIds), $fetchedIds));\n\n if (! empty($notFoundIds)) {\n $this->logger->info('[' . $this->getDisplayName() . '] Contact CRM IDs not found in HubSpot', [\n 'teamId' => $this->team->getId(),\n 'notFoundCount' => \\count($notFoundIds),\n 'notFoundIds' => $notFoundIds,\n 'requestedCount' => \\count($crmIds),\n 'fetchedCount' => \\count($allContacts),\n ]);\n }\n\n if (empty($allContacts)) {\n return $result;\n }\n\n $prepareStart = microtime(true);\n $accountMappings = $this->prepareAccountMappingsForContacts($allContacts);\n $prepareAccountsMs = (int) round((microtime(true) - $prepareStart) * 1000);\n\n $loopStart = microtime(true);\n foreach ($allContacts as $contactData) {\n $contactStart = microtime(true);\n\n try {\n $contact = $this->importContact($contactData, $accountMappings);\n if ($contact !== null) {\n $result['success_count']++;\n }\n } catch (\\Throwable $e) {\n $crmId = $contactData['id'] ?? 'unknown';\n $result['failed_ids'][] = $crmId;\n $result['errors'][$crmId] = $e->getMessage();\n\n $this->logger->warning('[' . $this->getDisplayName() . '] Failed to import contact', [\n 'teamId' => $this->team->getId(),\n 'crmId' => $crmId,\n 'error' => $e->getMessage(),\n ]);\n }\n\n $contactMs = (int) round((microtime(true) - $contactStart) * 1000);\n if ($contactMs > 1000) {\n $slowContacts[] = ['crmId' => $contactData['id'] ?? 'unknown', 'ms' => $contactMs];\n }\n }\n $loopMs = (int) round((microtime(true) - $loopStart) * 1000);\n $totalMs = (int) round((microtime(true) - $batchStart) * 1000);\n\n $this->logger->info('[' . $this->getDisplayName() . '] importContactBatch timing', [\n 'teamId' => $this->team->getId(),\n 'contact_count' => \\count($allContacts),\n 'requested_count' => \\count($crmIds),\n 'not_found_count' => \\count($notFoundIds),\n 'total_ms' => $totalMs,\n 'fetch_api_ms' => $fetchMs,\n 'prepare_accounts_ms' => $prepareAccountsMs,\n 'contacts_loop_ms' => $loopMs,\n 'avg_contact_ms' => ! empty($allContacts) ? (int) round($loopMs / \\count($allContacts)) : 0,\n 'slow_contacts_count' => \\count($slowContacts),\n 'slow_contacts' => array_slice($slowContacts, 0, 10),\n ]);\n\n return $result;\n }\n\n private function fetchContactsByIdsInChunks(array $crmIds): array\n {\n $fields = $this->getContactFields();\n $allContacts = [];\n\n foreach (array_chunk($crmIds, self::BATCH_SIZE) as $chunk) {\n try {\n $contacts = $this->client->getContactsByIds($chunk, $fields);\n foreach ($contacts as $contactData) {\n $allContacts[] = $contactData;\n }\n } catch (\\Throwable $e) {\n // @TODO what will happen if this exception is thrown\n $this->logger->warning('[' . $this->getDisplayName() . '] Batch contact fetch failed', [\n 'chunk_size' => \\count($chunk),\n 'error' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n }\n\n return $allContacts;\n }\n\n private function prepareAccountMappingsForContacts(array $contacts): array\n {\n $companyIds = [];\n foreach ($contacts as $contact) {\n $companyId = $contact['properties']['associatedcompanyid'] ?? null;\n if ($companyId !== null && $companyId !== '') {\n $companyIds[] = (string) $companyId;\n }\n }\n\n $companyIds = array_unique($companyIds);\n\n if (empty($companyIds)) {\n return [];\n }\n\n $mappings = $this->crmEntityRepository\n ->getExistingAccountIdsMap($this->config, $companyIds);\n\n $missingCompanyIds = array_diff($companyIds, array_keys($mappings));\n\n if (empty($missingCompanyIds)) {\n return $mappings;\n }\n\n $this->logger->info('[' . $this->getDisplayName() . '] Batch syncing missing accounts for contacts', [\n 'teamId' => $this->team->getId(),\n 'total_companies' => \\count($companyIds),\n 'existing_companies' => \\count($mappings),\n 'missing_companies' => \\count($missingCompanyIds),\n ]);\n\n try {\n $syncedAccounts = $this->batchSyncAccountsForContacts($missingCompanyIds);\n $mappings = array_merge($mappings, $syncedAccounts);\n } catch (\\Throwable $e) {\n $this->logger->error('[' . $this->getDisplayName() . '] Failed to batch sync missing accounts', [\n 'teamId' => $this->team->getId(),\n 'missingCompanyIds' => $missingCompanyIds,\n 'missingCount' => count($missingCompanyIds),\n 'error' => $e->getMessage(),\n ]);\n }\n\n return $mappings;\n }\n\n private function batchSyncAccountsForContacts(array $companyIds): array\n {\n $syncedAccounts = [];\n $fields = $this->getCompanyFields();\n\n foreach (array_chunk($companyIds, self::BATCH_SIZE) as $chunk) {\n try {\n $companies = $this->client->getCompaniesByIds($chunk, $fields);\n\n foreach ($companies as $companyData) {\n try {\n $account = $this->importAccount($companyData);\n if ($account) {\n $syncedAccounts[$account->getCrmProviderId()] = $account->getId();\n }\n } catch (\\Throwable $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Failed to import account in batch', [\n 'teamId' => $this->team->getId(),\n 'companyId' => $companyData['id'] ?? 'unknown',\n 'error' => $e->getMessage(),\n ]);\n }\n }\n } catch (\\Throwable $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Batch company fetch failed', [\n 'teamId' => $this->team->getId(),\n 'chunk_size' => \\count($chunk),\n 'error' => $e->getMessage(),\n ]);\n }\n }\n\n return $syncedAccounts;\n }\n\n /**\n * Process webhook-collected company batches.\n *\n * Drains Redis sets containing company CRM IDs collected from webhook events\n * and dispatches ImportAccountBatch jobs for batch processing.\n *\n * @return int Number of company IDs dispatched to jobs\n */\n public function batchSyncCompanies(): int\n {\n $configId = $this->team->getCrmConfiguration()->getId();\n\n return $this->batchProcessor->processBatchesForObjectType(\n WebhookSyncBatchProcessor::OBJECT_TYPE_COMPANY,\n $configId\n );\n }\n\n public function importAccountBatchByIds(array $crmIds): array\n {\n $result = [\n 'success_count' => 0,\n 'failed_ids' => [],\n 'errors' => [],\n ];\n\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $batchStart = microtime(true);\n $slowAccounts = [];\n\n $fields = $this->getCompanyFields();\n $allCompanies = [];\n\n $fetchStart = microtime(true);\n foreach (array_chunk($crmIds, self::BATCH_SIZE) as $chunk) {\n try {\n $companies = $this->client->getCompaniesByIds($chunk, $fields);\n foreach ($companies as $companyData) {\n $allCompanies[] = $companyData;\n }\n } catch (\\Throwable $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Batch company fetch failed', [\n 'chunk_size' => \\count($chunk),\n 'error' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n }\n $fetchMs = (int) round((microtime(true) - $fetchStart) * 1000);\n\n $fetchedIds = array_map('strval', array_column($allCompanies, 'id'));\n $notFoundIds = array_values(array_diff(array_map('strval', $crmIds), $fetchedIds));\n\n if (! empty($notFoundIds)) {\n $this->logger->info('[' . $this->getDisplayName() . '] Company CRM IDs not found in HubSpot', [\n 'teamId' => $this->team->getId(),\n 'notFoundCount' => \\count($notFoundIds),\n 'notFoundIds' => $notFoundIds,\n 'requestedCount' => \\count($crmIds),\n 'fetchedCount' => \\count($allCompanies),\n ]);\n }\n\n $loopStart = microtime(true);\n foreach ($allCompanies as $companyData) {\n $accountStart = microtime(true);\n\n try {\n $account = $this->importAccount($companyData);\n if ($account !== null) {\n $result['success_count']++;\n }\n } catch (\\Throwable $e) {\n $crmId = $companyData['id'] ?? $companyData['properties']['hs_object_id'] ?? 'unknown';\n $result['failed_ids'][] = $crmId;\n $result['errors'][$crmId] = $e->getMessage();\n\n $this->logger->warning('[' . $this->getDisplayName() . '] Failed to import account', [\n 'crmId' => $crmId,\n 'error' => $e->getMessage(),\n ]);\n }\n\n $accountMs = (int) round((microtime(true) - $accountStart) * 1000);\n if ($accountMs > 1000) {\n $crmId = $companyData['id'] ?? $companyData['properties']['hs_object_id'] ?? 'unknown';\n $slowAccounts[] = ['crmId' => $crmId, 'ms' => $accountMs];\n }\n }\n $loopMs = (int) round((microtime(true) - $loopStart) * 1000);\n $totalMs = (int) round((microtime(true) - $batchStart) * 1000);\n\n $this->logger->info('[' . $this->getDisplayName() . '] importAccountBatch timing', [\n 'teamId' => $this->team->getId(),\n 'account_count' => \\count($allCompanies),\n 'requested_count' => \\count($crmIds),\n 'not_found_count' => \\count($notFoundIds),\n 'total_ms' => $totalMs,\n 'fetch_api_ms' => $fetchMs,\n 'accounts_loop_ms' => $loopMs,\n 'avg_account_ms' => ! empty($allCompanies) ? (int) round($loopMs / \\count($allCompanies)) : 0,\n 'slow_accounts_count' => \\count($slowAccounts),\n 'slow_accounts' => array_slice($slowAccounts, 0, 10),\n ]);\n\n return $result;\n }\n\n private function getCompanyFields(): array\n {\n return [\n 'country',\n 'name',\n 'phone',\n 'domain',\n 'hs_avatar_filemanager_key',\n 'industry',\n 'hubspot_owner_id',\n 'hs_object_id',\n 'createdate',\n 'hs_lastmodifieddate',\n ];\n }\n\n private function importAccount($crmData): ?Account\n {\n $crmProviderId = $crmData['id'] ?? $crmData['properties']['hs_object_id'] ?? null;\n\n $this->logger->info('[HubSpot] importAccount', [\n 'crm_provider_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n if (empty($crmData['properties'])) {\n $this->logger->info('[HubSpot] importAccount failed: empty properties', [\n 'crm_provider_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n return null;\n }\n\n $properties = $crmData['properties'];\n $crmId = (string) $properties['hs_object_id'];\n\n $countryCode = isset($properties['country']) ? $this->convertCountryNameToCode($properties['country']) : null;\n\n if (isset($properties['phone'])) {\n // Trim to our width and attempt to parse it.\n $number = mb_strimwidth($properties['phone'], 0, 25);\n $parsedNumber = parsePhoneNumber($countryCode, $number);\n } else {\n $parsedNumber = [];\n }\n\n $name = '[unknown]';\n if (isset($properties['name'])) {\n $name = $properties['name'];\n }\n\n $photoPath = $this->prospectPhotoPathService->getOrGeneratePhotoPath(\n $this->config,\n $crmId,\n Account::class,\n $crmId,\n $name\n );\n\n $industry = null;\n if (isset($properties['industry'])) {\n $industry = mb_strimwidth($properties['industry'], 0, 40);\n }\n\n $ownerId = $profile = null;\n if (isset($properties['hubspot_owner_id'])) {\n $ownerId = $properties['hubspot_owner_id'];\n $profile = $this->crmEntityRepository->findProfileByExternalId($this->config, (string) $ownerId);\n }\n\n $domain = null;\n if (isset($properties['domain'])) {\n $domain = StringUtil::resolveDomain($properties['domain']);\n }\n\n $remotelyCreatedAt = null;\n if (isset($properties['createdate']) && ! empty($properties['createdate'])) {\n $remotelyCreatedAt = Carbon::parse($properties['createdate']);\n }\n\n $data = [\n 'crm_provider_id' => $crmId,\n 'team_id' => $this->team->id,\n 'user_id' => $profile ? $profile->user_id : null,\n 'owner_id' => $ownerId,\n 'name' => mb_strimwidth($name, 0, 191),\n 'photo_path' => $photoPath,\n 'industry' => $industry,\n 'domain' => $domain !== null\n ? substr($domain, 0, 191)\n : null,\n 'phone' => $parsedNumber['phone'] ?? null,\n 'ext' => $parsedNumber['ext'] ?? null,\n 'country_code' => $countryCode,\n 'remotely_created_at' => $remotelyCreatedAt,\n ];\n\n return $this->crmEntityRepository->importAccount($this->config, $data);\n }\n\n public function deleteContact(string $crmProviderId): bool\n {\n try {\n $contact = $this->crmEntityRepository->findContactByExternalId($this->config, $crmProviderId);\n\n if (! $contact) {\n $this->logger->info('[HubSpot] Contact not found for deletion', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n // return success because we do not know which instance is the target\n return true;\n }\n\n $id = $contact->getId();\n\n $this->logger->info('[HubSpot] Deleting contact via webhook', [\n 'contact_id' => $id,\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n $contact->delete();\n DeleteContactJob::dispatch($id)->afterCommit();\n\n return true;\n } catch (Exception $e) {\n $this->logger->error('[HubSpot] Failed to delete contact via webhook', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n return false;\n }\n }\n\n public function deleteAccount(string $crmProviderId): bool\n {\n try {\n $account = $this->crmEntityRepository->findAccountByExternalId($this->config, $crmProviderId);\n\n if (! $account) {\n $this->logger->info('[HubSpot] Account not found for deletion', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n // return success because we do not know which instance is the target\n return true;\n }\n\n $id = $account->getId();\n\n $this->logger->info('[HubSpot] Deleting account via webhook', [\n 'account_id' => $id,\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n $account->delete();\n DeleteAccountJob::dispatch($id)->afterCommit();\n\n return true;\n } catch (Exception $e) {\n $this->logger->error('[HubSpot] Failed to delete account via webhook', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n return false;\n }\n }\n\n public function deleteOpportunity(string $crmProviderId): bool\n {\n try {\n $opportunity = $this->crmEntityRepository->findOpportunityByExternalId($this->config, $crmProviderId);\n\n if (! $opportunity) {\n $this->logger->info('[HubSpot] Opportunity not found for deletion', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n // return success because we do not know which instance is the target\n return true;\n }\n\n $id = $opportunity->getId();\n\n $this->logger->info('[HubSpot] Deleting opportunity via webhook', [\n 'opportunity_id' => $id,\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n $opportunity->delete();\n DeleteOpportunityJob::dispatch($id)->afterCommit();\n\n return true;\n } catch (Exception $e) {\n $this->logger->error('[HubSpot] Failed to delete opportunity via webhook', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n return false;\n }\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"app ~/jiminny/app, folder","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".circleci, folder","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".cursor, folder","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".github","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".sonarlint, folder","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".vscode, folder","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".windsurf, folder","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"app, sources root","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Actions, folder","depth":8,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Component, folder","depth":8,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Acl, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ActionItems, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Activity, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ActivityAnalytics, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ActivitySearch, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"AiActivityType, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"AiAutomation, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"AiCallScoring, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"AskAnything, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Dtos, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Events, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"AskAnythingPromptService.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"HistoryService.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"AskJiminnyAi, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"AWS, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"BillingManagement, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Cache, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"CoachingFeedback, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Country, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"CustomerApi, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Database, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Datadog, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"DateTime, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"DealInsights, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"DealRisks, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ElasticSearch","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Eloquent","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Encoding","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Encryption","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ES","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Faker","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"FeatureFlags","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"FFMpeg","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"FileSystem","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Gecko","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Gong","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"GuzzleHttp","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"KeyPoints","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Kiosk","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"LanguageDetection","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"LiveFeed","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Locks","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Math","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"MediaPipeline","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"MeetingBot, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"MobileSettings, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Model, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Notification, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Nudge, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ParagraphBreaker, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ParticipantSpeech, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"PartitionedCookie, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"PlaybackPage, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Playlist, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Prophet, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ProphetAi, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ProsperWorks, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Queue, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Job, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"RateLimitAware.php, abstract class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"RateLimitAwareWrapper.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"BotsQueueConstants.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Constants.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ProcessingQueueConstants.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Router, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Saml2, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"SCIM, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Seeder, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Sentry, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Serializer, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Settings, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Sidekick, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Slack, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"TeamInsights, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"TimeMemoryMapper, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Transcription, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"TranscriptionSummary, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Twilio, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Uploader, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"UrlGenerator, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Utility, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Exceptions, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Service, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"BaseRateLimiter.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"EfficientJsonParser.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ProviderRateLimiter.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"RateLimiterInstance.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Uuid, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Waveform, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Webhooks, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Workflow, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Configuration, folder","depth":8,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Console, folder","depth":8,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Commands, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Activities, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Analytics, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Calendars, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Crm, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Hubspot, folder","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"IntegrationApp, folder","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Traits, folder","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"AddLayoutEntities.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"AutologDelayedCommand.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"BullhornCommandAbstract.php, abstract class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"BullhornPingCommand.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"BullhornSearchCommand.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"BullhornSessionCommand.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"CheckActivityLoggableCommand.php, final class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"CleanDuplicateFieldDataCommand.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"FullSyncOpportunityCommand.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"LogActivitiesCommand.php, final class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ManageSyncStrategyCommand.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"MatchCrmObjectsCommand.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"MatchOpportunityActivitiesCommand.php, class","depth":11,"on_screen":false,"role_description":"text"}]...
|
-7650338153608560940
|
5036038088437336294
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
Editor for custom.log
Sync Changes
Hide This Notification
Code changed:
Hide
Analyzing…
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\ServiceTraits;
use Carbon\Carbon;
use Exception;
use Illuminate\Support\Str;
use Jiminny\Exceptions\CrmException;
use Jiminny\Jobs\Crm\Delete\DeleteAccountJob;
use Jiminny\Jobs\Crm\Delete\DeleteContactJob;
use Jiminny\Jobs\Crm\Delete\DeleteOpportunityJob;
use Jiminny\Models\Account;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Repositories\Crm\CrmEntityRepository;
use Jiminny\Services\Avatar\ProspectPhotoPathService;
use Jiminny\Services\Crm\Hubspot\HubspotClientInterface;
use Jiminny\Services\Crm\Hubspot\WebhookSyncBatchProcessor;
use Jiminny\Utils\StringUtil;
trait SyncCrmEntitiesTrait
{
use OpportunitySyncTrait;
private const string CDN_URL = '[URL_WITH_CREDENTIALS] Carbon $since Fetch contacts modified after this date
* @param Carbon|null $to Optional end date for modification range
*
* @return int Number of contacts successfully synced
*/
public function syncContacts(Carbon $since, ?Carbon $to = null): int
{
$syncCount = 0;
try {
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$fields = $this->getContactFields();
$payload = $this->payloadBuilder->getRecentlyUpdatedSearchPayload($since, $to, $fields);
foreach ($this->client->getPaginatedDataGenerator($payload, 'contacts') as $hsContact) {
$this->importContact($hsContact);
$syncCount++;
}
} catch (Exception $exception) {
$this->logger->error('[' . $this->getDisplayName() . '] Sync contacts failed', [
'teamId' => $this->team->getUuid(),
'reason' => $exception->getMessage(),
]);
}
return $syncCount;
}
/**
* @inheritdoc
*/
public function syncContact(string $crmId): ?Contact
{
try {
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$fields = $this->getContactFields();
$hsContact = $this->client->getContactById($crmId, $fields);
} catch (\HubSpot\Client\Crm\Contacts\ApiException $e) {
$this->logger->info('[' . $this->getDisplayName() . '] Contact fetch failed', [
'teamId' => $this->team->getUuid(),
'crmId' => $crmId,
'reason' => $e->getMessage(),
]);
return null;
} catch (CrmException $e) {
$this->logger->info('[' . $this->getDisplayName() . '] Contact not found', [
'teamId' => $this->team->getUuid(),
'crmId' => $crmId,
'reason' => $e->getMessage(),
]);
return null;
}
if (empty($hsContact['properties']) || empty($hsContact['id'])) {
$this->logger->warning('[' . $this->getDisplayName() . '] Contact data incomplete', [
'teamId' => $this->team->getUuid(),
'crmId' => $crmId,
'has_properties' => ! empty($hsContact['properties']),
'has_id' => ! empty($hsContact['id']),
]);
return null;
}
return $this->importContact($hsContact);
}
private function getContactFields(): array
{
return [
'associatedcompanyid',
'country',
'firstname',
'lastname',
'phone',
'mobilephone',
'email',
'photo',
'hs_avatar_filemanager_key',
'jobtitle',
'hubspot_owner_id',
'hs_object_id',
'createdate',
'hs_lastmodifieddate',
];
}
/**
* @inheritdoc
*/
private function importContact($crmData, array $accountMappings = []): ?Contact
{
$crmProviderId = $crmData['id'] ?? null;
$this->logger->info('[HubSpot] importContact', [
'crm_provider_id' => $crmProviderId,
'config_id' => $this->config->getId(),
]);
if (empty($crmData['properties'])) {
$this->logger->info('[HubSpot] importContact failed: empty properties', [
'crm_provider_id' => $crmProviderId,
'config_id' => $this->config->getId(),
]);
return null;
}
$properties = $crmData['properties'];
$crmId = (string) $crmData['id'];
$accountId = $this->resolveContactAccount($properties, $accountMappings);
$data = $this->buildContactData($crmId, $properties, $accountId);
return $this->crmEntityRepository->importContact($this->config, $data);
}
private function resolveContactAccount(array $properties, array $accountMappings): ?int
{
if (empty($properties['associatedcompanyid'])) {
return null;
}
$companyId = (string) $properties['associatedcompanyid'];
if (! empty($accountMappings)) {
return $accountMappings[$companyId] ?? null;
}
return $this->crmEntityRepository->findAccountByExternalId(
$this->team->getCrmConfiguration(),
$companyId
)?->getId() ?? $this->syncAccount($companyId)?->getId();
}
private function buildContactData(string $crmId, array $properties, ?int $accountId): array
{
$countryCode = $this->buildContactCountry($properties);
$name = $this->buildContactName($properties);
$photoPath = $this->teamService->generateAvatar(
$crmId,
empty($name) ? ($properties['email'] ?? 'N/A') : $name,
);
$parsedNumber = $this->buildContactPhone($countryCode, $properties);
$mobileNumber = $this->buildContactMobilePhone($countryCode, $properties);
$ownerId = $properties['hubspot_owner_id'] ?? null;
$profile = $ownerId !== null
? $this->crmEntityRepository->findProfileByExternalId($this->config, (string) $ownerId)
: null;
$ext = (isset($parsedNumber['ext']) && is_string($parsedNumber['ext']) && strlen($parsedNumber['ext']) <= 10)
? $parsedNumber['ext']
: null;
$title = isset($properties['jobtitle']) ? mb_strimwidth($properties['jobtitle'], 0, 128) : null;
$email = isset($properties['email']) ? mb_strimwidth($properties['email'], 0, 191) : null;
$remotelyCreatedAt = ! empty($properties['createdate']) ? Carbon::parse($properties['createdate']) : null;
return [
'crm_provider_id' => $crmId,
'team_id' => $this->team->getId(),
'account_id' => $accountId,
'user_id' => $profile?->getUserId(),
'owner_id' => $ownerId,
'name' => $name,
'title' => $title,
'email' => $email,
'country_code' => $countryCode,
'phone' => $parsedNumber['phone'] ?? null,
'mobile_phone' => $mobileNumber ?? null,
'ext' => $ext,
'photo_path' => $photoPath,
'remotely_created_at' => $remotelyCreatedAt,
];
}
/**
* @param $properties
*/
private function buildContactName($properties): string
{
if (is_array($properties)) {
return $this->buildContactNameFromArray($properties);
}
return $this->buildContactNameFromObject($properties);
}
private function buildContactNameFromArray(array $properties): string
{
if (! empty($properties['name'])) {
return mb_strimwidth($properties['name'], 0, 100);
}
$name = '';
if (! empty($properties['firstname'])) {
$name = $properties['firstname'] . ' ';
}
if (! empty($properties['lastname'])) {
$name .= $properties['lastname'];
}
if ($name === '' && ! empty($properties['email'])) {
$name = $properties['email'];
}
return mb_strimwidth($name, 0, 100);
}
private function buildContactNameFromObject($properties): string
{
$name = '';
if (isset($properties->firstname)) {
$name = $properties->firstname->value . ' ';
}
if (isset($properties->lastname)) {
$name .= $properties->lastname->value;
}
if ($name === '' && isset($properties->email)) {
$name = $properties->email->value;
}
return mb_strimwidth($name, 0, 100);
}
/**
* @param $properties
*/
private function buildContactPhone(?string $countryCode, $properties): ?array
{
if (is_array($properties) && empty($properties['phone']) === false) {
$number = mb_strimwidth($properties['phone'], 0, 25);
return parsePhoneNumber($countryCode, $number);
} elseif (isset($properties->phone)) {
$number = mb_strimwidth($properties->phone->value, 0, 25);
return parsePhoneNumber($countryCode, $number);
}
return [];
}
/**
* @param $properties
*/
private function buildContactMobilePhone(?string $countryCode, $properties): ?string
{
return isset($properties['mobilephone'])
? Str::limit(phone_e164($countryCode, $properties['mobilephone']), 25, '')
: null;
}
/**
* @param $properties
* @param $account
*/
private function buildContactCountry($properties): ?string
{
if (is_array($properties) && empty($properties['country']) === false) {
return $this->convertCountryNameToCode($properties['country']);
}
if (isset($properties->country)) {
return $this->convertCountryNameToCode($properties->country->value);
}
return null;
}
/**
* HubSpot doesn't have leads, so this method does nothing.
*
* @param Carbon $since
* @param Carbon|null $to
* @param string|null $crmProfileId
*
* @return int
*/
public function syncLeads(Carbon $since, ?Carbon $to = null, ?string $crmProfileId = null): int
{
// Mark unused parameters to avoid code smell warnings
unset($since, $to, $crmProfileId);
return 0;
}
/**
* HubSpot doesn't have leads.
*
* @param string $crmId
*
* @inheritdoc
*/
public function syncLead(string $crmId): ?Lead
{
// Mark unused parameter to avoid code smell warnings
unset($crmId);
return null;
}
/**
* Sync accounts (companies) modified since a given date (manual sync mode).
*
* This method fetches companies from HubSpot API based on modification date and
* imports them one by one. It is used for:
* - Manual sync commands (e.g., crm:sync-account with --from parameter)
* - Initial sync for new teams
* - Backfill operations
*
* For regular sync webhook batchSyncCompanies is used:
*
* @param Carbon $since Fetch companies modified after this date
* @param Carbon|null $to Optional end date for modification range
*
* @return int Number of companies successfully synced
*/
public function syncAccounts(Carbon $since, ?Carbon $to = null): int
{
$syncCount = 0;
try {
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$fields = $this->getCompanyFields();
$payload = $this->payloadBuilder->getRecentlyUpdatedSearchPayload($since, $to, $fields);
foreach ($this->client->getPaginatedDataGenerator($payload, 'companies') as $hsAccount) {
$this->importAccount($hsAccount);
$syncCount++;
}
} catch (Exception $exception) {
$this->logger->error('[' . $this->getDisplayName() . '] Sync accounts failed', [
'teamId' => $this->team->getUuid(),
'reason' => $exception->getMessage(),
]);
}
return $syncCount;
}
/**
* @inheritdoc
*/
public function syncAccount(string $crmId): ?Account
{
try {
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$fields = $this->getCompanyFields();
$hsAccount = $this->client->getAccountById($crmId, $fields);
} catch (\HubSpot\Client\Crm\Companies\ApiException $e) {
$this->logger->info('[' . $this->getDisplayName() . '] Account fetch failed', [
'teamId' => $this->team->getUuid(),
'crmId' => $crmId,
'reason' => $e->getMessage(),
]);
return null;
} catch (CrmException $e) {
$this->logger->info('[' . $this->getDisplayName() . '] Account not found', [
'teamId' => $this->team->getUuid(),
'crmId' => $crmId,
'reason' => $e->getMessage(),
]);
return null;
}
return $this->importAccount($hsAccount);
}
/**
* Process webhook-collected contact batches.
*
* Drains Redis sets containing contact CRM IDs collected from webhook events
* and dispatches ImportContactBatch jobs for batch processing.
*
* @return int Number of contact IDs dispatched to jobs
*/
public function batchSyncContacts(): int
{
$configId = $this->team->getCrmConfiguration()->getId();
return $this->batchProcessor->processBatchesForObjectType(
WebhookSyncBatchProcessor::OBJECT_TYPE_CONTACT,
$configId
);
}
public function importContactBatchByIds(array $crmIds): array
{
$result = [
'success_count' => 0,
'failed_ids' => [],
'errors' => [],
];
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$batchStart = microtime(true);
$slowContacts = [];
$fetchStart = microtime(true);
$allContacts = $this->fetchContactsByIdsInChunks($crmIds);
$fetchMs = (int) round((microtime(true) - $fetchStart) * 1000);
$fetchedIds = array_map('strval', array_column($allContacts, 'id'));
$notFoundIds = array_values(array_diff(array_map('strval', $crmIds), $fetchedIds));
if (! empty($notFoundIds)) {
$this->logger->info('[' . $this->getDisplayName() . '] Contact CRM IDs not found in HubSpot', [
'teamId' => $this->team->getId(),
'notFoundCount' => \count($notFoundIds),
'notFoundIds' => $notFoundIds,
'requestedCount' => \count($crmIds),
'fetchedCount' => \count($allContacts),
]);
}
if (empty($allContacts)) {
return $result;
}
$prepareStart = microtime(true);
$accountMappings = $this->prepareAccountMappingsForContacts($allContacts);
$prepareAccountsMs = (int) round((microtime(true) - $prepareStart) * 1000);
$loopStart = microtime(true);
foreach ($allContacts as $contactData) {
$contactStart = microtime(true);
try {
$contact = $this->importContact($contactData, $accountMappings);
if ($contact !== null) {
$result['success_count']++;
}
} catch (\Throwable $e) {
$crmId = $contactData['id'] ?? 'unknown';
$result['failed_ids'][] = $crmId;
$result['errors'][$crmId] = $e->getMessage();
$this->logger->warning('[' . $this->getDisplayName() . '] Failed to import contact', [
'teamId' => $this->team->getId(),
'crmId' => $crmId,
'error' => $e->getMessage(),
]);
}
$contactMs = (int) round((microtime(true) - $contactStart) * 1000);
if ($contactMs > 1000) {
$slowContacts[] = ['crmId' => $contactData['id'] ?? 'unknown', 'ms' => $contactMs];
}
}
$loopMs = (int) round((microtime(true) - $loopStart) * 1000);
$totalMs = (int) round((microtime(true) - $batchStart) * 1000);
$this->logger->info('[' . $this->getDisplayName() . '] importContactBatch timing', [
'teamId' => $this->team->getId(),
'contact_count' => \count($allContacts),
'requested_count' => \count($crmIds),
'not_found_count' => \count($notFoundIds),
'total_ms' => $totalMs,
'fetch_api_ms' => $fetchMs,
'prepare_accounts_ms' => $prepareAccountsMs,
'contacts_loop_ms' => $loopMs,
'avg_contact_ms' => ! empty($allContacts) ? (int) round($loopMs / \count($allContacts)) : 0,
'slow_contacts_count' => \count($slowContacts),
'slow_contacts' => array_slice($slowContacts, 0, 10),
]);
return $result;
}
private function fetchContactsByIdsInChunks(array $crmIds): array
{
$fields = $this->getContactFields();
$allContacts = [];
foreach (array_chunk($crmIds, self::BATCH_SIZE) as $chunk) {
try {
$contacts = $this->client->getContactsByIds($chunk, $fields);
foreach ($contacts as $contactData) {
$allContacts[] = $contactData;
}
} catch (\Throwable $e) {
// @TODO what will happen if this exception is thrown
$this->logger->warning('[' . $this->getDisplayName() . '] Batch contact fetch failed', [
'chunk_size' => \count($chunk),
'error' => $e->getMessage(),
]);
throw $e;
}
}
return $allContacts;
}
private function prepareAccountMappingsForContacts(array $contacts): array
{
$companyIds = [];
foreach ($contacts as $contact) {
$companyId = $contact['properties']['associatedcompanyid'] ?? null;
if ($companyId !== null && $companyId !== '') {
$companyIds[] = (string) $companyId;
}
}
$companyIds = array_unique($companyIds);
if (empty($companyIds)) {
return [];
}
$mappings = $this->crmEntityRepository
->getExistingAccountIdsMap($this->config, $companyIds);
$missingCompanyIds = array_diff($companyIds, array_keys($mappings));
if (empty($missingCompanyIds)) {
return $mappings;
}
$this->logger->info('[' . $this->getDisplayName() . '] Batch syncing missing accounts for contacts', [
'teamId' => $this->team->getId(),
'total_companies' => \count($companyIds),
'existing_companies' => \count($mappings),
'missing_companies' => \count($missingCompanyIds),
]);
try {
$syncedAccounts = $this->batchSyncAccountsForContacts($missingCompanyIds);
$mappings = array_merge($mappings, $syncedAccounts);
} catch (\Throwable $e) {
$this->logger->error('[' . $this->getDisplayName() . '] Failed to batch sync missing accounts', [
'teamId' => $this->team->getId(),
'missingCompanyIds' => $missingCompanyIds,
'missingCount' => count($missingCompanyIds),
'error' => $e->getMessage(),
]);
}
return $mappings;
}
private function batchSyncAccountsForContacts(array $companyIds): array
{
$syncedAccounts = [];
$fields = $this->getCompanyFields();
foreach (array_chunk($companyIds, self::BATCH_SIZE) as $chunk) {
try {
$companies = $this->client->getCompaniesByIds($chunk, $fields);
foreach ($companies as $companyData) {
try {
$account = $this->importAccount($companyData);
if ($account) {
$syncedAccounts[$account->getCrmProviderId()] = $account->getId();
}
} catch (\Throwable $e) {
$this->logger->warning('[' . $this->getDisplayName() . '] Failed to import account in batch', [
'teamId' => $this->team->getId(),
'companyId' => $companyData['id'] ?? 'unknown',
'error' => $e->getMessage(),
]);
}
}
} catch (\Throwable $e) {
$this->logger->warning('[' . $this->getDisplayName() . '] Batch company fetch failed', [
'teamId' => $this->team->getId(),
'chunk_size' => \count($chunk),
'error' => $e->getMessage(),
]);
}
}
return $syncedAccounts;
}
/**
* Process webhook-collected company batches.
*
* Drains Redis sets containing company CRM IDs collected from webhook events
* and dispatches ImportAccountBatch jobs for batch processing.
*
* @return int Number of company IDs dispatched to jobs
*/
public function batchSyncCompanies(): int
{
$configId = $this->team->getCrmConfiguration()->getId();
return $this->batchProcessor->processBatchesForObjectType(
WebhookSyncBatchProcessor::OBJECT_TYPE_COMPANY,
$configId
);
}
public function importAccountBatchByIds(array $crmIds): array
{
$result = [
'success_count' => 0,
'failed_ids' => [],
'errors' => [],
];
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$batchStart = microtime(true);
$slowAccounts = [];
$fields = $this->getCompanyFields();
$allCompanies = [];
$fetchStart = microtime(true);
foreach (array_chunk($crmIds, self::BATCH_SIZE) as $chunk) {
try {
$companies = $this->client->getCompaniesByIds($chunk, $fields);
foreach ($companies as $companyData) {
$allCompanies[] = $companyData;
}
} catch (\Throwable $e) {
$this->logger->warning('[' . $this->getDisplayName() . '] Batch company fetch failed', [
'chunk_size' => \count($chunk),
'error' => $e->getMessage(),
]);
throw $e;
}
}
$fetchMs = (int) round((microtime(true) - $fetchStart) * 1000);
$fetchedIds = array_map('strval', array_column($allCompanies, 'id'));
$notFoundIds = array_values(array_diff(array_map('strval', $crmIds), $fetchedIds));
if (! empty($notFoundIds)) {
$this->logger->info('[' . $this->getDisplayName() . '] Company CRM IDs not found in HubSpot', [
'teamId' => $this->team->getId(),
'notFoundCount' => \count($notFoundIds),
'notFoundIds' => $notFoundIds,
'requestedCount' => \count($crmIds),
'fetchedCount' => \count($allCompanies),
]);
}
$loopStart = microtime(true);
foreach ($allCompanies as $companyData) {
$accountStart = microtime(true);
try {
$account = $this->importAccount($companyData);
if ($account !== null) {
$result['success_count']++;
}
} catch (\Throwable $e) {
$crmId = $companyData['id'] ?? $companyData['properties']['hs_object_id'] ?? 'unknown';
$result['failed_ids'][] = $crmId;
$result['errors'][$crmId] = $e->getMessage();
$this->logger->warning('[' . $this->getDisplayName() . '] Failed to import account', [
'crmId' => $crmId,
'error' => $e->getMessage(),
]);
}
$accountMs = (int) round((microtime(true) - $accountStart) * 1000);
if ($accountMs > 1000) {
$crmId = $companyData['id'] ?? $companyData['properties']['hs_object_id'] ?? 'unknown';
$slowAccounts[] = ['crmId' => $crmId, 'ms' => $accountMs];
}
}
$loopMs = (int) round((microtime(true) - $loopStart) * 1000);
$totalMs = (int) round((microtime(true) - $batchStart) * 1000);
$this->logger->info('[' . $this->getDisplayName() . '] importAccountBatch timing', [
'teamId' => $this->team->getId(),
'account_count' => \count($allCompanies),
'requested_count' => \count($crmIds),
'not_found_count' => \count($notFoundIds),
'total_ms' => $totalMs,
'fetch_api_ms' => $fetchMs,
'accounts_loop_ms' => $loopMs,
'avg_account_ms' => ! empty($allCompanies) ? (int) round($loopMs / \count($allCompanies)) : 0,
'slow_accounts_count' => \count($slowAccounts),
'slow_accounts' => array_slice($slowAccounts, 0, 10),
]);
return $result;
}
private function getCompanyFields(): array
{
return [
'country',
'name',
'phone',
'domain',
'hs_avatar_filemanager_key',
'industry',
'hubspot_owner_id',
'hs_object_id',
'createdate',
'hs_lastmodifieddate',
];
}
private function importAccount($crmData): ?Account
{
$crmProviderId = $crmData['id'] ?? $crmData['properties']['hs_object_id'] ?? null;
$this->logger->info('[HubSpot] importAccount', [
'crm_provider_id' => $crmProviderId,
'config_id' => $this->config->getId(),
]);
if (empty($crmData['properties'])) {
$this->logger->info('[HubSpot] importAccount failed: empty properties', [
'crm_provider_id' => $crmProviderId,
'config_id' => $this->config->getId(),
]);
return null;
}
$properties = $crmData['properties'];
$crmId = (string) $properties['hs_object_id'];
$countryCode = isset($properties['country']) ? $this->convertCountryNameToCode($properties['country']) : null;
if (isset($properties['phone'])) {
// Trim to our width and attempt to parse it.
$number = mb_strimwidth($properties['phone'], 0, 25);
$parsedNumber = parsePhoneNumber($countryCode, $number);
} else {
$parsedNumber = [];
}
$name = '[unknown]';
if (isset($properties['name'])) {
$name = $properties['name'];
}
$photoPath = $this->prospectPhotoPathService->getOrGeneratePhotoPath(
$this->config,
$crmId,
Account::class,
$crmId,
$name
);
$industry = null;
if (isset($properties['industry'])) {
$industry = mb_strimwidth($properties['industry'], 0, 40);
}
$ownerId = $profile = null;
if (isset($properties['hubspot_owner_id'])) {
$ownerId = $properties['hubspot_owner_id'];
$profile = $this->crmEntityRepository->findProfileByExternalId($this->config, (string) $ownerId);
}
$domain = null;
if (isset($properties['domain'])) {
$domain = StringUtil::resolveDomain($properties['domain']);
}
$remotelyCreatedAt = null;
if (isset($properties['createdate']) && ! empty($properties['createdate'])) {
$remotelyCreatedAt = Carbon::parse($properties['createdate']);
}
$data = [
'crm_provider_id' => $crmId,
'team_id' => $this->team->id,
'user_id' => $profile ? $profile->user_id : null,
'owner_id' => $ownerId,
'name' => mb_strimwidth($name, 0, 191),
'photo_path' => $photoPath,
'industry' => $industry,
'domain' => $domain !== null
? substr($domain, 0, 191)
: null,
'phone' => $parsedNumber['phone'] ?? null,
'ext' => $parsedNumber['ext'] ?? null,
'country_code' => $countryCode,
'remotely_created_at' => $remotelyCreatedAt,
];
return $this->crmEntityRepository->importAccount($this->config, $data);
}
public function deleteContact(string $crmProviderId): bool
{
try {
$contact = $this->crmEntityRepository->findContactByExternalId($this->config, $crmProviderId);
if (! $contact) {
$this->logger->info('[HubSpot] Contact not found for deletion', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
// return success because we do not know which instance is the target
return true;
}
$id = $contact->getId();
$this->logger->info('[HubSpot] Deleting contact via webhook', [
'contact_id' => $id,
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
$contact->delete();
DeleteContactJob::dispatch($id)->afterCommit();
return true;
} catch (Exception $e) {
$this->logger->error('[HubSpot] Failed to delete contact via webhook', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
'error' => $e->getMessage(),
]);
return false;
}
}
public function deleteAccount(string $crmProviderId): bool
{
try {
$account = $this->crmEntityRepository->findAccountByExternalId($this->config, $crmProviderId);
if (! $account) {
$this->logger->info('[HubSpot] Account not found for deletion', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
// return success because we do not know which instance is the target
return true;
}
$id = $account->getId();
$this->logger->info('[HubSpot] Deleting account via webhook', [
'account_id' => $id,
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
$account->delete();
DeleteAccountJob::dispatch($id)->afterCommit();
return true;
} catch (Exception $e) {
$this->logger->error('[HubSpot] Failed to delete account via webhook', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
'error' => $e->getMessage(),
]);
return false;
}
}
public function deleteOpportunity(string $crmProviderId): bool
{
try {
$opportunity = $this->crmEntityRepository->findOpportunityByExternalId($this->config, $crmProviderId);
if (! $opportunity) {
$this->logger->info('[HubSpot] Opportunity not found for deletion', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
// return success because we do not know which instance is the target
return true;
}
$id = $opportunity->getId();
$this->logger->info('[HubSpot] Deleting opportunity via webhook', [
'opportunity_id' => $id,
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
$opportunity->delete();
DeleteOpportunityJob::dispatch($id)->afterCommit();
return true;
} catch (Exception $e) {
$this->logger->error('[HubSpot] Failed to delete opportunity via webhook', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
'error' => $e->getMessage(),
]);
return false;
}
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide
app ~/jiminny/app, folder
.circleci, folder
.cursor, folder
.github
.sonarlint, folder
.vscode, folder
.windsurf, folder
app, sources root
Actions, folder
Component, folder
Acl, folder
ActionItems, folder
Activity, folder
ActivityAnalytics, folder
ActivitySearch, folder
AiActivityType, folder
AiAutomation, folder
AiCallScoring, folder
AskAnything, folder
Dtos, folder
Events, folder
AskAnythingPromptService.php
HistoryService.php
AskJiminnyAi, folder
AWS, folder
BillingManagement, folder
Cache, folder
CoachingFeedback, folder
Country, folder
CustomerApi, folder
Database, folder
Datadog, folder
DateTime, folder
DealInsights, folder
DealRisks, folder
ElasticSearch
Eloquent
Encoding
Encryption
ES
Faker
FeatureFlags
FFMpeg
FileSystem
Gecko
Gong
GuzzleHttp
KeyPoints
Kiosk
LanguageDetection
LiveFeed
Locks
Math
MediaPipeline
MeetingBot, folder
MobileSettings, folder
Model, folder
Notification, folder
Nudge, folder
ParagraphBreaker, folder
ParticipantSpeech, folder
PartitionedCookie, folder
PlaybackPage, folder
Playlist, folder
Prophet, folder
ProphetAi, folder
ProsperWorks, folder
Queue, folder
Job, folder
RateLimitAware.php, abstract class
RateLimitAwareWrapper.php, class
BotsQueueConstants.php
Constants.php
ProcessingQueueConstants.php
Router, folder
Saml2, folder
SCIM, folder
Seeder, folder
Sentry, folder
Serializer, folder
Settings, folder
Sidekick, folder
Slack, folder
TeamInsights, folder
TimeMemoryMapper, folder
Transcription, folder
TranscriptionSummary, folder
Twilio, folder
Uploader, folder
UrlGenerator, folder
Utility, folder
Exceptions, folder
Service, folder
BaseRateLimiter.php, class
EfficientJsonParser.php, class
ProviderRateLimiter.php, class
RateLimiterInstance.php, class
Uuid, folder
Waveform, folder
Webhooks, folder
Workflow, folder
Configuration, folder
Console, folder
Commands, folder
Activities, folder
Analytics, folder
Calendars, folder
Crm, folder
Hubspot, folder
IntegrationApp, folder
Traits, folder
AddLayoutEntities.php, class
AutologDelayedCommand.php, class
BullhornCommandAbstract.php, abstract class
BullhornPingCommand.php, class
BullhornSearchCommand.php, class
BullhornSessionCommand.php, class
CheckActivityLoggableCommand.php, final class
CleanDuplicateFieldDataCommand.php, class
FullSyncOpportunityCommand.php, class
LogActivitiesCommand.php, final class
ManageSyncStrategyCommand.php, class
MatchCrmObjectsCommand.php, class
MatchOpportunityActivitiesCommand.php, class...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
2841
|
114
|
35
|
2026-05-07T11:43:03.305261+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778154183305_m2.jpg...
|
PhpStorm
|
faVsco.js – SyncCrmEntitiesTrait.php
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
Editor for custom.log
Sync Changes
Hide This Notification
Code changed:
Hide
Analyzing…
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\ServiceTraits;
use Carbon\Carbon;
use Exception;
use Illuminate\Support\Str;
use Jiminny\Exceptions\CrmException;
use Jiminny\Jobs\Crm\Delete\DeleteAccountJob;
use Jiminny\Jobs\Crm\Delete\DeleteContactJob;
use Jiminny\Jobs\Crm\Delete\DeleteOpportunityJob;
use Jiminny\Models\Account;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Repositories\Crm\CrmEntityRepository;
use Jiminny\Services\Avatar\ProspectPhotoPathService;
use Jiminny\Services\Crm\Hubspot\HubspotClientInterface;
use Jiminny\Services\Crm\Hubspot\WebhookSyncBatchProcessor;
use Jiminny\Utils\StringUtil;
trait SyncCrmEntitiesTrait
{
use OpportunitySyncTrait;
private const string CDN_URL = '[URL_WITH_CREDENTIALS] Carbon $since Fetch contacts modified after this date
* @param Carbon|null $to Optional end date for modification range
*
* @return int Number of contacts successfully synced
*/
public function syncContacts(Carbon $since, ?Carbon $to = null): int
{
$syncCount = 0;
try {
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$fields = $this->getContactFields();
$payload = $this->payloadBuilder->getRecentlyUpdatedSearchPayload($since, $to, $fields);
foreach ($this->client->getPaginatedDataGenerator($payload, 'contacts') as $hsContact) {
$this->importContact($hsContact);
$syncCount++;
}
} catch (Exception $exception) {
$this->logger->error('[' . $this->getDisplayName() . '] Sync contacts failed', [
'teamId' => $this->team->getUuid(),
'reason' => $exception->getMessage(),
]);
}
return $syncCount;
}
/**
* @inheritdoc
*/
public function syncContact(string $crmId): ?Contact
{
try {
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$fields = $this->getContactFields();
$hsContact = $this->client->getContactById($crmId, $fields);
} catch (\HubSpot\Client\Crm\Contacts\ApiException $e) {
$this->logger->info('[' . $this->getDisplayName() . '] Contact fetch failed', [
'teamId' => $this->team->getUuid(),
'crmId' => $crmId,
'reason' => $e->getMessage(),
]);
return null;
} catch (CrmException $e) {
$this->logger->info('[' . $this->getDisplayName() . '] Contact not found', [
'teamId' => $this->team->getUuid(),
'crmId' => $crmId,
'reason' => $e->getMessage(),
]);
return null;
}
if (empty($hsContact['properties']) || empty($hsContact['id'])) {
$this->logger->warning('[' . $this->getDisplayName() . '] Contact data incomplete', [
'teamId' => $this->team->getUuid(),
'crmId' => $crmId,
'has_properties' => ! empty($hsContact['properties']),
'has_id' => ! empty($hsContact['id']),
]);
return null;
}
return $this->importContact($hsContact);
}
private function getContactFields(): array
{
return [
'associatedcompanyid',
'country',
'firstname',
'lastname',
'phone',
'mobilephone',
'email',
'photo',
'hs_avatar_filemanager_key',
'jobtitle',
'hubspot_owner_id',
'hs_object_id',
'createdate',
'hs_lastmodifieddate',
];
}
/**
* @inheritdoc
*/
private function importContact($crmData, array $accountMappings = []): ?Contact
{
$crmProviderId = $crmData['id'] ?? null;
$this->logger->info('[HubSpot] importContact', [
'crm_provider_id' => $crmProviderId,
'config_id' => $this->config->getId(),
]);
if (empty($crmData['properties'])) {
$this->logger->info('[HubSpot] importContact failed: empty properties', [
'crm_provider_id' => $crmProviderId,
'config_id' => $this->config->getId(),
]);
return null;
}
$properties = $crmData['properties'];
$crmId = (string) $crmData['id'];
$accountId = $this->resolveContactAccount($properties, $accountMappings);
$data = $this->buildContactData($crmId, $properties, $accountId);
return $this->crmEntityRepository->importContact($this->config, $data);
}
private function resolveContactAccount(array $properties, array $accountMappings): ?int
{
if (empty($properties['associatedcompanyid'])) {
return null;
}
$companyId = (string) $properties['associatedcompanyid'];
if (! empty($accountMappings)) {
return $accountMappings[$companyId] ?? null;
}
return $this->crmEntityRepository->findAccountByExternalId(
$this->team->getCrmConfiguration(),
$companyId
)?->getId() ?? $this->syncAccount($companyId)?->getId();
}
private function buildContactData(string $crmId, array $properties, ?int $accountId): array
{
$countryCode = $this->buildContactCountry($properties);
$name = $this->buildContactName($properties);
$photoPath = $this->teamService->generateAvatar(
$crmId,
empty($name) ? ($properties['email'] ?? 'N/A') : $name,
);
$parsedNumber = $this->buildContactPhone($countryCode, $properties);
$mobileNumber = $this->buildContactMobilePhone($countryCode, $properties);
$ownerId = $properties['hubspot_owner_id'] ?? null;
$profile = $ownerId !== null
? $this->crmEntityRepository->findProfileByExternalId($this->config, (string) $ownerId)
: null;
$ext = (isset($parsedNumber['ext']) && is_string($parsedNumber['ext']) && strlen($parsedNumber['ext']) <= 10)
? $parsedNumber['ext']
: null;
$title = isset($properties['jobtitle']) ? mb_strimwidth($properties['jobtitle'], 0, 128) : null;
$email = isset($properties['email']) ? mb_strimwidth($properties['email'], 0, 191) : null;
$remotelyCreatedAt = ! empty($properties['createdate']) ? Carbon::parse($properties['createdate']) : null;
return [
'crm_provider_id' => $crmId,
'team_id' => $this->team->getId(),
'account_id' => $accountId,
'user_id' => $profile?->getUserId(),
'owner_id' => $ownerId,
'name' => $name,
'title' => $title,
'email' => $email,
'country_code' => $countryCode,
'phone' => $parsedNumber['phone'] ?? null,
'mobile_phone' => $mobileNumber ?? null,
'ext' => $ext,
'photo_path' => $photoPath,
'remotely_created_at' => $remotelyCreatedAt,
];
}
/**
* @param $properties
*/
private function buildContactName($properties): string
{
if (is_array($properties)) {
return $this->buildContactNameFromArray($properties);
}
return $this->buildContactNameFromObject($properties);
}
private function buildContactNameFromArray(array $properties): string
{
if (! empty($properties['name'])) {
return mb_strimwidth($properties['name'], 0, 100);
}
$name = '';
if (! empty($properties['firstname'])) {
$name = $properties['firstname'] . ' ';
}
if (! empty($properties['lastname'])) {
$name .= $properties['lastname'];
}
if ($name === '' && ! empty($properties['email'])) {
$name = $properties['email'];
}
return mb_strimwidth($name, 0, 100);
}
private function buildContactNameFromObject($properties): string
{
$name = '';
if (isset($properties->firstname)) {
$name = $properties->firstname->value . ' ';
}
if (isset($properties->lastname)) {
$name .= $properties->lastname->value;
}
if ($name === '' && isset($properties->email)) {
$name = $properties->email->value;
}
return mb_strimwidth($name, 0, 100);
}
/**
* @param $properties
*/
private function buildContactPhone(?string $countryCode, $properties): ?array
{
if (is_array($properties) && empty($properties['phone']) === false) {
$number = mb_strimwidth($properties['phone'], 0, 25);
return parsePhoneNumber($countryCode, $number);
} elseif (isset($properties->phone)) {
$number = mb_strimwidth($properties->phone->value, 0, 25);
return parsePhoneNumber($countryCode, $number);
}
return [];
}
/**
* @param $properties
*/
private function buildContactMobilePhone(?string $countryCode, $properties): ?string
{
return isset($properties['mobilephone'])
? Str::limit(phone_e164($countryCode, $properties['mobilephone']), 25, '')
: null;
}
/**
* @param $properties
* @param $account
*/
private function buildContactCountry($properties): ?string
{
if (is_array($properties) && empty($properties['country']) === false) {
return $this->convertCountryNameToCode($properties['country']);
}
if (isset($properties->country)) {
return $this->convertCountryNameToCode($properties->country->value);
}
return null;
}
/**
* HubSpot doesn't have leads, so this method does nothing.
*
* @param Carbon $since
* @param Carbon|null $to
* @param string|null $crmProfileId
*
* @return int
*/
public function syncLeads(Carbon $since, ?Carbon $to = null, ?string $crmProfileId = null): int
{
// Mark unused parameters to avoid code smell warnings
unset($since, $to, $crmProfileId);
return 0;
}
/**
* HubSpot doesn't have leads.
*
* @param string $crmId
*
* @inheritdoc
*/
public function syncLead(string $crmId): ?Lead
{
// Mark unused parameter to avoid code smell warnings
unset($crmId);
return null;
}
/**
* Sync accounts (companies) modified since a given date (manual sync mode).
*
* This method fetches companies from HubSpot API based on modification date and
* imports them one by one. It is used for:
* - Manual sync commands (e.g., crm:sync-account with --from parameter)
* - Initial sync for new teams
* - Backfill operations
*
* For regular sync webhook batchSyncCompanies is used:
*
* @param Carbon $since Fetch companies modified after this date
* @param Carbon|null $to Optional end date for modification range
*
* @return int Number of companies successfully synced
*/
public function syncAccounts(Carbon $since, ?Carbon $to = null): int
{
$syncCount = 0;
try {
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$fields = $this->getCompanyFields();
$payload = $this->payloadBuilder->getRecentlyUpdatedSearchPayload($since, $to, $fields);
foreach ($this->client->getPaginatedDataGenerator($payload, 'companies') as $hsAccount) {
$this->importAccount($hsAccount);
$syncCount++;
}
} catch (Exception $exception) {
$this->logger->error('[' . $this->getDisplayName() . '] Sync accounts failed', [
'teamId' => $this->team->getUuid(),
'reason' => $exception->getMessage(),
]);
}
return $syncCount;
}
/**
* @inheritdoc
*/
public function syncAccount(string $crmId): ?Account
{
try {
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$fields = $this->getCompanyFields();
$hsAccount = $this->client->getAccountById($crmId, $fields);
} catch (\HubSpot\Client\Crm\Companies\ApiException $e) {
$this->logger->info('[' . $this->getDisplayName() . '] Account fetch failed', [
'teamId' => $this->team->getUuid(),
'crmId' => $crmId,
'reason' => $e->getMessage(),
]);
return null;
} catch (CrmException $e) {
$this->logger->info('[' . $this->getDisplayName() . '] Account not found', [
'teamId' => $this->team->getUuid(),
'crmId' => $crmId,
'reason' => $e->getMessage(),
]);
return null;
}
return $this->importAccount($hsAccount);
}
/**
* Process webhook-collected contact batches.
*
* Drains Redis sets containing contact CRM IDs collected from webhook events
* and dispatches ImportContactBatch jobs for batch processing.
*
* @return int Number of contact IDs dispatched to jobs
*/
public function batchSyncContacts(): int
{
$configId = $this->team->getCrmConfiguration()->getId();
return $this->batchProcessor->processBatchesForObjectType(
WebhookSyncBatchProcessor::OBJECT_TYPE_CONTACT,
$configId
);
}
public function importContactBatchByIds(array $crmIds): array
{
$result = [
'success_count' => 0,
'failed_ids' => [],
'errors' => [],
];
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$batchStart = microtime(true);
$slowContacts = [];
$fetchStart = microtime(true);
$allContacts = $this->fetchContactsByIdsInChunks($crmIds);
$fetchMs = (int) round((microtime(true) - $fetchStart) * 1000);
$fetchedIds = array_map('strval', array_column($allContacts, 'id'));
$notFoundIds = array_values(array_diff(array_map('strval', $crmIds), $fetchedIds));
if (! empty($notFoundIds)) {
$this->logger->info('[' . $this->getDisplayName() . '] Contact CRM IDs not found in HubSpot', [
'teamId' => $this->team->getId(),
'notFoundCount' => \count($notFoundIds),
'notFoundIds' => $notFoundIds,
'requestedCount' => \count($crmIds),
'fetchedCount' => \count($allContacts),
]);
}
if (empty($allContacts)) {
return $result;
}
$prepareStart = microtime(true);
$accountMappings = $this->prepareAccountMappingsForContacts($allContacts);
$prepareAccountsMs = (int) round((microtime(true) - $prepareStart) * 1000);
$loopStart = microtime(true);
foreach ($allContacts as $contactData) {
$contactStart = microtime(true);
try {
$contact = $this->importContact($contactData, $accountMappings);
if ($contact !== null) {
$result['success_count']++;
}
} catch (\Throwable $e) {
$crmId = $contactData['id'] ?? 'unknown';
$result['failed_ids'][] = $crmId;
$result['errors'][$crmId] = $e->getMessage();
$this->logger->warning('[' . $this->getDisplayName() . '] Failed to import contact', [
'teamId' => $this->team->getId(),
'crmId' => $crmId,
'error' => $e->getMessage(),
]);
}
$contactMs = (int) round((microtime(true) - $contactStart) * 1000);
if ($contactMs > 1000) {
$slowContacts[] = ['crmId' => $contactData['id'] ?? 'unknown', 'ms' => $contactMs];
}
}
$loopMs = (int) round((microtime(true) - $loopStart) * 1000);
$totalMs = (int) round((microtime(true) - $batchStart) * 1000);
$this->logger->info('[' . $this->getDisplayName() . '] importContactBatch timing', [
'teamId' => $this->team->getId(),
'contact_count' => \count($allContacts),
'requested_count' => \count($crmIds),
'not_found_count' => \count($notFoundIds),
'total_ms' => $totalMs,
'fetch_api_ms' => $fetchMs,
'prepare_accounts_ms' => $prepareAccountsMs,
'contacts_loop_ms' => $loopMs,
'avg_contact_ms' => ! empty($allContacts) ? (int) round($loopMs / \count($allContacts)) : 0,
'slow_contacts_count' => \count($slowContacts),
'slow_contacts' => array_slice($slowContacts, 0, 10),
]);
return $result;
}
private function fetchContactsByIdsInChunks(array $crmIds): array
{
$fields = $this->getContactFields();
$allContacts = [];
foreach (array_chunk($crmIds, self::BATCH_SIZE) as $chunk) {
try {
$contacts = $this->client->getContactsByIds($chunk, $fields);
foreach ($contacts as $contactData) {
$allContacts[] = $contactData;
}
} catch (\Throwable $e) {
// @TODO what will happen if this exception is thrown
$this->logger->warning('[' . $this->getDisplayName() . '] Batch contact fetch failed', [
'chunk_size' => \count($chunk),
'error' => $e->getMessage(),
]);
throw $e;
}
}
return $allContacts;
}
private function prepareAccountMappingsForContacts(array $contacts): array
{
$companyIds = [];
foreach ($contacts as $contact) {
$companyId = $contact['properties']['associatedcompanyid'] ?? null;
if ($companyId !== null && $companyId !== '') {
$companyIds[] = (string) $companyId;
}
}
$companyIds = array_unique($companyIds);
if (empty($companyIds)) {
return [];
}
$mappings = $this->crmEntityRepository
->getExistingAccountIdsMap($this->config, $companyIds);
$missingCompanyIds = array_diff($companyIds, array_keys($mappings));
if (empty($missingCompanyIds)) {
return $mappings;
}
$this->logger->info('[' . $this->getDisplayName() . '] Batch syncing missing accounts for contacts', [
'teamId' => $this->team->getId(),
'total_companies' => \count($companyIds),
'existing_companies' => \count($mappings),
'missing_companies' => \count($missingCompanyIds),
]);
try {
$syncedAccounts = $this->batchSyncAccountsForContacts($missingCompanyIds);
$mappings = array_merge($mappings, $syncedAccounts);
} catch (\Throwable $e) {
$this->logger->error('[' . $this->getDisplayName() . '] Failed to batch sync missing accounts', [
'teamId' => $this->team->getId(),
'missingCompanyIds' => $missingCompanyIds,
'missingCount' => count($missingCompanyIds),
'error' => $e->getMessage(),
]);
}
return $mappings;
}
private function batchSyncAccountsForContacts(array $companyIds): array
{
$syncedAccounts = [];
$fields = $this->getCompanyFields();
foreach (array_chunk($companyIds, self::BATCH_SIZE) as $chunk) {
try {
$companies = $this->client->getCompaniesByIds($chunk, $fields);
foreach ($companies as $companyData) {
try {
$account = $this->importAccount($companyData);
if ($account) {
$syncedAccounts[$account->getCrmProviderId()] = $account->getId();
}
} catch (\Throwable $e) {
$this->logger->warning('[' . $this->getDisplayName() . '] Failed to import account in batch', [
'teamId' => $this->team->getId(),
'companyId' => $companyData['id'] ?? 'unknown',
'error' => $e->getMessage(),
]);
}
}
} catch (\Throwable $e) {
$this->logger->warning('[' . $this->getDisplayName() . '] Batch company fetch failed', [
'teamId' => $this->team->getId(),
'chunk_size' => \count($chunk),
'error' => $e->getMessage(),
]);
}
}
return $syncedAccounts;
}
/**
* Process webhook-collected company batches.
*
* Drains Redis sets containing company CRM IDs collected from webhook events
* and dispatches ImportAccountBatch jobs for batch processing.
*
* @return int Number of company IDs dispatched to jobs
*/
public function batchSyncCompanies(): int
{
$configId = $this->team->getCrmConfiguration()->getId();
return $this->batchProcessor->processBatchesForObjectType(
WebhookSyncBatchProcessor::OBJECT_TYPE_COMPANY,
$configId
);
}
public function importAccountBatchByIds(array $crmIds): array
{
$result = [
'success_count' => 0,
'failed_ids' => [],
'errors' => [],
];
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$batchStart = microtime(true);
$slowAccounts = [];
$fields = $this->getCompanyFields();
$allCompanies = [];
$fetchStart = microtime(true);
foreach (array_chunk($crmIds, self::BATCH_SIZE) as $chunk) {
try {
$companies = $this->client->getCompaniesByIds($chunk, $fields);
foreach ($companies as $companyData) {
$allCompanies[] = $companyData;
}
} catch (\Throwable $e) {
$this->logger->warning('[' . $this->getDisplayName() . '] Batch company fetch failed', [
'chunk_size' => \count($chunk),
'error' => $e->getMessage(),
]);
throw $e;
}
}
$fetchMs = (int) round((microtime(true) - $fetchStart) * 1000);
$fetchedIds = array_map('strval', array_column($allCompanies, 'id'));
$notFoundIds = array_values(array_diff(array_map('strval', $crmIds), $fetchedIds));
if (! empty($notFoundIds)) {
$this->logger->info('[' . $this->getDisplayName() . '] Company CRM IDs not found in HubSpot', [
'teamId' => $this->team->getId(),
'notFoundCount' => \count($notFoundIds),
'notFoundIds' => $notFoundIds,
'requestedCount' => \count($crmIds),
'fetchedCount' => \count($allCompanies),
]);
}
$loopStart = microtime(true);
foreach ($allCompanies as $companyData) {
$accountStart = microtime(true);
try {
$account = $this->importAccount($companyData);
if ($account !== null) {
$result['success_count']++;
}
} catch (\Throwable $e) {
$crmId = $companyData['id'] ?? $companyData['properties']['hs_object_id'] ?? 'unknown';
$result['failed_ids'][] = $crmId;
$result['errors'][$crmId] = $e->getMessage();
$this->logger->warning('[' . $this->getDisplayName() . '] Failed to import account', [
'crmId' => $crmId,
'error' => $e->getMessage(),
]);
}
$accountMs = (int) round((microtime(true) - $accountStart) * 1000);
if ($accountMs > 1000) {
$crmId = $companyData['id'] ?? $companyData['properties']['hs_object_id'] ?? 'unknown';
$slowAccounts[] = ['crmId' => $crmId, 'ms' => $accountMs];
}
}
$loopMs = (int) round((microtime(true) - $loopStart) * 1000);
$totalMs = (int) round((microtime(true) - $batchStart) * 1000);
$this->logger->info('[' . $this->getDisplayName() . '] importAccountBatch timing', [
'teamId' => $this->team->getId(),
'account_count' => \count($allCompanies),
'requested_count' => \count($crmIds),
'not_found_count' => \count($notFoundIds),
'total_ms' => $totalMs,
'fetch_api_ms' => $fetchMs,
'accounts_loop_ms' => $loopMs,
'avg_account_ms' => ! empty($allCompanies) ? (int) round($loopMs / \count($allCompanies)) : 0,
'slow_accounts_count' => \count($slowAccounts),
'slow_accounts' => array_slice($slowAccounts, 0, 10),
]);
return $result;
}
private function getCompanyFields(): array
{
return [
'country',
'name',
'phone',
'domain',
'hs_avatar_filemanager_key',
'industry',
'hubspot_owner_id',
'hs_object_id',
'createdate',
'hs_lastmodifieddate',
];
}
private function importAccount($crmData): ?Account
{
$crmProviderId = $crmData['id'] ?? $crmData['properties']['hs_object_id'] ?? null;
$this->logger->info('[HubSpot] importAccount', [
'crm_provider_id' => $crmProviderId,
'config_id' => $this->config->getId(),
]);
if (empty($crmData['properties'])) {
$this->logger->info('[HubSpot] importAccount failed: empty properties', [
'crm_provider_id' => $crmProviderId,
'config_id' => $this->config->getId(),
]);
return null;
}
$properties = $crmData['properties'];
$crmId = (string) $properties['hs_object_id'];
$countryCode = isset($properties['country']) ? $this->convertCountryNameToCode($properties['country']) : null;
if (isset($properties['phone'])) {
// Trim to our width and attempt to parse it.
$number = mb_strimwidth($properties['phone'], 0, 25);
$parsedNumber = parsePhoneNumber($countryCode, $number);
} else {
$parsedNumber = [];
}
$name = '[unknown]';
if (isset($properties['name'])) {
$name = $properties['name'];
}
$photoPath = $this->prospectPhotoPathService->getOrGeneratePhotoPath(
$this->config,
$crmId,
Account::class,
$crmId,
$name
);
$industry = null;
if (isset($properties['industry'])) {
$industry = mb_strimwidth($properties['industry'], 0, 40);
}
$ownerId = $profile = null;
if (isset($properties['hubspot_owner_id'])) {
$ownerId = $properties['hubspot_owner_id'];
$profile = $this->crmEntityRepository->findProfileByExternalId($this->config, (string) $ownerId);
}
$domain = null;
if (isset($properties['domain'])) {
$domain = StringUtil::resolveDomain($properties['domain']);
}
$remotelyCreatedAt = null;
if (isset($properties['createdate']) && ! empty($properties['createdate'])) {
$remotelyCreatedAt = Carbon::parse($properties['createdate']);
}
$data = [
'crm_provider_id' => $crmId,
'team_id' => $this->team->id,
'user_id' => $profile ? $profile->user_id : null,
'owner_id' => $ownerId,
'name' => mb_strimwidth($name, 0, 191),
'photo_path' => $photoPath,
'industry' => $industry,
'domain' => $domain !== null
? substr($domain, 0, 191)
: null,
'phone' => $parsedNumber['phone'] ?? null,
'ext' => $parsedNumber['ext'] ?? null,
'country_code' => $countryCode,
'remotely_created_at' => $remotelyCreatedAt,
];
return $this->crmEntityRepository->importAccount($this->config, $data);
}
public function deleteContact(string $crmProviderId): bool
{
try {
$contact = $this->crmEntityRepository->findContactByExternalId($this->config, $crmProviderId);
if (! $contact) {
$this->logger->info('[HubSpot] Contact not found for deletion', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
// return success because we do not know which instance is the target
return true;
}
$id = $contact->getId();
$this->logger->info('[HubSpot] Deleting contact via webhook', [
'contact_id' => $id,
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
$contact->delete();
DeleteContactJob::dispatch($id)->afterCommit();
return true;
} catch (Exception $e) {
$this->logger->error('[HubSpot] Failed to delete contact via webhook', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
'error' => $e->getMessage(),
]);
return false;
}
}
public function deleteAccount(string $crmProviderId): bool
{
try {
$account = $this->crmEntityRepository->findAccountByExternalId($this->config, $crmProviderId);
if (! $account) {
$this->logger->info('[HubSpot] Account not found for deletion', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
// return success because we do not know which instance is the target
return true;
}
$id = $account->getId();
$this->logger->info('[HubSpot] Deleting account via webhook', [
'account_id' => $id,
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
$account->delete();
DeleteAccountJob::dispatch($id)->afterCommit();
return true;
} catch (Exception $e) {
$this->logger->error('[HubSpot] Failed to delete account via webhook', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
'error' => $e->getMessage(),
]);
return false;
}
}
public function deleteOpportunity(string $crmProviderId): bool
{
try {
$opportunity = $this->crmEntityRepository->findOpportunityByExternalId($this->config, $crmProviderId);
if (! $opportunity) {
$this->logger->info('[HubSpot] Opportunity not found for deletion', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
// return success because we do not know which instance is the target
return true;
}
$id = $opportunity->getId();
$this->logger->info('[HubSpot] Deleting opportunity via webhook', [
'opportunity_id' => $id,
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
$opportunity->delete();
DeleteOpportunityJob::dispatch($id)->afterCommit();
return true;
} catch (Exception $e) {
$this->logger->error('[HubSpot] Failed to delete opportunity via webhook', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
'error' => $e->getMessage(),
]);
return false;
}
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide
app ~/jiminny/app, folder
.circleci, folder
.cursor, folder
.github
.sonarlint, folder
.vscode, folder
.windsurf, folder
app, sources root
Actions, folder
Component, folder
Acl, folder
ActionItems, folder
Activity, folder
ActivityAnalytics, folder
ActivitySearch, folder
AiActivityType, folder
AiAutomation, folder
AiCallScoring, folder
AskAnything, folder
Dtos, folder
Events, folder
AskAnythingPromptService.php
HistoryService.php
AskJiminnyAi, folder
AWS, folder
BillingManagement, folder
Cache, folder
CoachingFeedback, folder
Country, folder
CustomerApi, folder
Database, folder
Datadog, folder
DateTime, folder
DealInsights, folder
DealRisks, folder
ElasticSearch
Eloquent
Encoding
Encryption
ES
Faker
FeatureFlags
FFMpeg
FileSystem
Gecko
Gong
GuzzleHttp
KeyPoints
Kiosk
LanguageDetection
LiveFeed
Locks
Math
MediaPipeline
MeetingBot, folder
MobileSettings, folder
Model, folder
Notification, folder
Nudge, folder
ParagraphBreaker, folder
ParticipantSpeech, folder
PartitionedCookie, folder
PlaybackPage, folder
Playlist, folder
Prophet, folder
ProphetAi, folder
ProsperWorks, folder
Queue, folder
Job, folder
RateLimitAware.php, abstract class
RateLimitAwareWrapper.php, class
BotsQueueConstants.php
Constants.php
ProcessingQueueConstants.php
Router, folder
Saml2, folder
SCIM, folder
Seeder, folder
Sentry, folder
Serializer, folder
Settings, folder
Sidekick, folder
Slack, folder
TeamInsights, folder
TimeMemoryMapper, folder
Transcription, folder
TranscriptionSummary, folder
Twilio, folder
Uploader, folder
UrlGenerator, folder
Utility, folder
Exceptions, folder
Service, folder
BaseRateLimiter.php, class
EfficientJsonParser.php, class
ProviderRateLimiter.php, class
RateLimiterInstance.php, class
Uuid, folder
Waveform, folder
Webhooks, folder
Workflow, folder
Configuration, folder
Console, folder
Commands, folder
Activities, folder
Analytics, folder
Calendars, folder
Crm, folder
Hubspot, folder
IntegrationApp, folder
Traits, folder
AddLayoutEntities.php, class
AutologDelayedCommand.php, class
BullhornCommandAbstract.php, abstract class
BullhornPingCommand.php, class
BullhornSearchCommand.php, class
BullhornSessionCommand.php, class
CheckActivityLoggableCommand.php, final class
CleanDuplicateFieldDataCommand.php, class
FullSyncOpportunityCommand.php, class
LogActivitiesCommand.php, final class
ManageSyncStrategyCommand.php, class
MatchCrmObjectsCommand.php, class
MatchOpportunityActivitiesCommand.php, class
MigrateProvider.php, class
ProcessHubspotObjectsSyncBatches.php, class
PurgeDeletedOpportunitiesCommand.php, class
ResetGovernorLimits.php, class
SendNotLogged.php, class
SetupActivityTypeForFollowUp.php, final class
SetupCloseCrm.php, class
SetupCopperCrm.php, class
SetupCrmCommand.php, abstract class
SetupLayouts.php, class
SyncAccount.php, class
SyncContact.php, class
SyncFieldMetadata.php, class
SyncHubspotActiveDeals.php, class
SyncHubspotObjects.php, class
SyncLead.php, class
SyncObjects.php, class
SyncOpportunitiesMissingFieldDataCommand.php, class
SyncOpportunity.php, class
SyncProfileMetadata.php, class
SyncTeamMetadata.php, class
UpdateOpportunitySpecifications.php, class
DealInsights, folder
Dev, folder
AddRateLimitCommand.php, class
FixHubSpotTokens.php, class
FixMissMatchedCrmActivitiesCommand.php, final class
ImportCallsCommand.php, class
MonitorSocialAccountsState.php, class
Dialers, folder
DTOs, folder
Elasticsearch, folder
EngagementStats, folder
GeckoExport, folder
Livestream, folder
Mailboxes, folder
Migrate, folder
PlaybackThemes, folder
Playbooks, folder
Playlists, folder
Postmark, folder
ProphetAi, folder
Reports, folder
AutomatedReportsCommand.php, class
AutomatedReportsRetentionPolicyCommand.php, class
AutomatedReportsSendCommand.php, class
CreateMockAskJiminnyReportResultCommand.php, class
DeleteReportCommand.php, class
GenerateMarketingReport.php, class
Team.php, class
Usage.php, class
Slack, folder
Teams, folder
Tracks, folder
Transcription, folder
Twilio, folder
Users, folder
Vocabulary, folder
Zoom, folder
Command.php
CreateDatabaseUsers.php
DatabaseTableCount.php
DeleteOldAiCrmNotesCommand.php
DeleteS3LeftoversCommand.php
DevPostmanCommand.php
DiarizeViaAiParticipantIdentificationCommand.php
EncryptTokensCommand.php
EngagementStatsRegenerateCommand.php
FeatureFlagsHelper.php
FixCrossTenantIssues.php
FlushRolesPermissionsCache.php
GenerateInternalWebhookToken.php
GroupSetDefaultLanguageCommand.php
HelperTruncateCoachingTables.php
HubspotJournalPollingCommand.php
HubspotWebhookServiceCommand.php
ImportRecording.php
ImportUsersFromCsvFile.php
IterateUsersCommand.php
JiminnyCacheClearCommand.php
JiminnyDebugCommand.php
JiminnySetEncryptedTokenManagerModeCommand.php
JiminnyTokenInfoCommand.php
MakeSlackLiveCoachingChatNotesOn.php
ManageScimForTeam.php
MarkBranchForEnvironmentPipelineCommand.php
MuteOrganizerChannel.php, class
PhpApm.php, class
PropagateCoachingFeedbackCreatedAtToSectionFeedbacks.php, class...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.034242023,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: master","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"Editor for custom.log","depth":4,"bounds":{"left":0.5475399,"top":0.0726257,"width":0.44082448,"height":0.9066241},"on_screen":true,"role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Analyzing…","depth":4,"bounds":{"left":0.5053192,"top":0.17478053,"width":0.019946808,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits;\n\nuse Carbon\\Carbon;\nuse Exception;\nuse Illuminate\\Support\\Str;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Jobs\\Crm\\Delete\\DeleteAccountJob;\nuse Jiminny\\Jobs\\Crm\\Delete\\DeleteContactJob;\nuse Jiminny\\Jobs\\Crm\\Delete\\DeleteOpportunityJob;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Repositories\\Crm\\CrmEntityRepository;\nuse Jiminny\\Services\\Avatar\\ProspectPhotoPathService;\nuse Jiminny\\Services\\Crm\\Hubspot\\HubspotClientInterface;\nuse Jiminny\\Services\\Crm\\Hubspot\\WebhookSyncBatchProcessor;\nuse Jiminny\\Utils\\StringUtil;\n\ntrait SyncCrmEntitiesTrait\n{\n use OpportunitySyncTrait;\n private const string CDN_URL = 'https://cdn2.hubspot.net/';\n protected CrmEntityRepository $crmEntityRepository;\n protected ProspectPhotoPathService $prospectPhotoPathService;\n\n private function getAssociationDataForCollection(array $collection, string $fromObject, string $toObject): array\n {\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $hsOpportunityIds = array_column($collection, 'id');\n\n return $this->client->getAssociationsData($hsOpportunityIds, $fromObject, $toObject);\n }\n\n private function importAssociationData(array $collection, array $associatedData): array\n {\n $data = [];\n if (! empty($associatedData[$collection['id']])) {\n foreach ($associatedData[$collection['id']] as $id) {\n $data[] = [\n 'id' => $id,\n ];\n }\n }\n\n return ['results' => $data];\n }\n\n /**\n * Sync contacts modified since a given date (manual sync mode).\n *\n * This method fetches contacts from HubSpot API based on modification date and\n * imports them one by one. It is used for:\n * - Manual sync commands (e.g., crm:sync-contact with --from parameter)\n * - Initial sync for new teams\n * - Backfill operations\n *\n * For regular sync webhook batchSyncContacts is used:\n *\n * @param Carbon $since Fetch contacts modified after this date\n * @param Carbon|null $to Optional end date for modification range\n *\n * @return int Number of contacts successfully synced\n */\n public function syncContacts(Carbon $since, ?Carbon $to = null): int\n {\n $syncCount = 0;\n\n try {\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $fields = $this->getContactFields();\n $payload = $this->payloadBuilder->getRecentlyUpdatedSearchPayload($since, $to, $fields);\n\n foreach ($this->client->getPaginatedDataGenerator($payload, 'contacts') as $hsContact) {\n $this->importContact($hsContact);\n $syncCount++;\n }\n } catch (Exception $exception) {\n $this->logger->error('[' . $this->getDisplayName() . '] Sync contacts failed', [\n 'teamId' => $this->team->getUuid(),\n 'reason' => $exception->getMessage(),\n ]);\n }\n\n return $syncCount;\n }\n\n /**\n * @inheritdoc\n */\n public function syncContact(string $crmId): ?Contact\n {\n try {\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $fields = $this->getContactFields();\n $hsContact = $this->client->getContactById($crmId, $fields);\n } catch (\\HubSpot\\Client\\Crm\\Contacts\\ApiException $e) {\n $this->logger->info('[' . $this->getDisplayName() . '] Contact fetch failed', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n return null;\n } catch (CrmException $e) {\n $this->logger->info('[' . $this->getDisplayName() . '] Contact not found', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n return null;\n }\n\n if (empty($hsContact['properties']) || empty($hsContact['id'])) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Contact data incomplete', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'has_properties' => ! empty($hsContact['properties']),\n 'has_id' => ! empty($hsContact['id']),\n ]);\n\n return null;\n }\n\n return $this->importContact($hsContact);\n }\n\n private function getContactFields(): array\n {\n return [\n 'associatedcompanyid',\n 'country',\n 'firstname',\n 'lastname',\n 'phone',\n 'mobilephone',\n 'email',\n 'photo',\n 'hs_avatar_filemanager_key',\n 'jobtitle',\n 'hubspot_owner_id',\n 'hs_object_id',\n 'createdate',\n 'hs_lastmodifieddate',\n ];\n }\n\n /**\n * @inheritdoc\n */\n private function importContact($crmData, array $accountMappings = []): ?Contact\n {\n $crmProviderId = $crmData['id'] ?? null;\n\n $this->logger->info('[HubSpot] importContact', [\n 'crm_provider_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n if (empty($crmData['properties'])) {\n $this->logger->info('[HubSpot] importContact failed: empty properties', [\n 'crm_provider_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n return null;\n }\n\n $properties = $crmData['properties'];\n $crmId = (string) $crmData['id'];\n\n $accountId = $this->resolveContactAccount($properties, $accountMappings);\n $data = $this->buildContactData($crmId, $properties, $accountId);\n\n return $this->crmEntityRepository->importContact($this->config, $data);\n }\n\n private function resolveContactAccount(array $properties, array $accountMappings): ?int\n {\n if (empty($properties['associatedcompanyid'])) {\n return null;\n }\n\n $companyId = (string) $properties['associatedcompanyid'];\n\n if (! empty($accountMappings)) {\n return $accountMappings[$companyId] ?? null;\n }\n\n return $this->crmEntityRepository->findAccountByExternalId(\n $this->team->getCrmConfiguration(),\n $companyId\n )?->getId() ?? $this->syncAccount($companyId)?->getId();\n }\n\n private function buildContactData(string $crmId, array $properties, ?int $accountId): array\n {\n $countryCode = $this->buildContactCountry($properties);\n $name = $this->buildContactName($properties);\n $photoPath = $this->teamService->generateAvatar(\n $crmId,\n empty($name) ? ($properties['email'] ?? 'N/A') : $name,\n );\n $parsedNumber = $this->buildContactPhone($countryCode, $properties);\n $mobileNumber = $this->buildContactMobilePhone($countryCode, $properties);\n\n $ownerId = $properties['hubspot_owner_id'] ?? null;\n $profile = $ownerId !== null\n ? $this->crmEntityRepository->findProfileByExternalId($this->config, (string) $ownerId)\n : null;\n\n $ext = (isset($parsedNumber['ext']) && is_string($parsedNumber['ext']) && strlen($parsedNumber['ext']) <= 10)\n ? $parsedNumber['ext']\n : null;\n\n $title = isset($properties['jobtitle']) ? mb_strimwidth($properties['jobtitle'], 0, 128) : null;\n $email = isset($properties['email']) ? mb_strimwidth($properties['email'], 0, 191) : null;\n $remotelyCreatedAt = ! empty($properties['createdate']) ? Carbon::parse($properties['createdate']) : null;\n\n return [\n 'crm_provider_id' => $crmId,\n 'team_id' => $this->team->getId(),\n 'account_id' => $accountId,\n 'user_id' => $profile?->getUserId(),\n 'owner_id' => $ownerId,\n 'name' => $name,\n 'title' => $title,\n 'email' => $email,\n 'country_code' => $countryCode,\n 'phone' => $parsedNumber['phone'] ?? null,\n 'mobile_phone' => $mobileNumber ?? null,\n 'ext' => $ext,\n 'photo_path' => $photoPath,\n 'remotely_created_at' => $remotelyCreatedAt,\n ];\n }\n\n /**\n * @param $properties\n */\n private function buildContactName($properties): string\n {\n if (is_array($properties)) {\n return $this->buildContactNameFromArray($properties);\n }\n\n return $this->buildContactNameFromObject($properties);\n }\n\n private function buildContactNameFromArray(array $properties): string\n {\n if (! empty($properties['name'])) {\n return mb_strimwidth($properties['name'], 0, 100);\n }\n\n $name = '';\n if (! empty($properties['firstname'])) {\n $name = $properties['firstname'] . ' ';\n }\n\n if (! empty($properties['lastname'])) {\n $name .= $properties['lastname'];\n }\n\n if ($name === '' && ! empty($properties['email'])) {\n $name = $properties['email'];\n }\n\n return mb_strimwidth($name, 0, 100);\n }\n\n private function buildContactNameFromObject($properties): string\n {\n $name = '';\n if (isset($properties->firstname)) {\n $name = $properties->firstname->value . ' ';\n }\n if (isset($properties->lastname)) {\n $name .= $properties->lastname->value;\n }\n if ($name === '' && isset($properties->email)) {\n $name = $properties->email->value;\n }\n\n return mb_strimwidth($name, 0, 100);\n }\n\n /**\n * @param $properties\n */\n private function buildContactPhone(?string $countryCode, $properties): ?array\n {\n if (is_array($properties) && empty($properties['phone']) === false) {\n $number = mb_strimwidth($properties['phone'], 0, 25);\n\n return parsePhoneNumber($countryCode, $number);\n } elseif (isset($properties->phone)) {\n $number = mb_strimwidth($properties->phone->value, 0, 25);\n\n return parsePhoneNumber($countryCode, $number);\n }\n\n return [];\n }\n\n /**\n * @param $properties\n */\n private function buildContactMobilePhone(?string $countryCode, $properties): ?string\n {\n return isset($properties['mobilephone'])\n ? Str::limit(phone_e164($countryCode, $properties['mobilephone']), 25, '')\n : null;\n }\n\n /**\n * @param $properties\n * @param $account\n */\n private function buildContactCountry($properties): ?string\n {\n if (is_array($properties) && empty($properties['country']) === false) {\n return $this->convertCountryNameToCode($properties['country']);\n }\n\n if (isset($properties->country)) {\n return $this->convertCountryNameToCode($properties->country->value);\n }\n\n return null;\n }\n\n /**\n * HubSpot doesn't have leads, so this method does nothing.\n *\n * @param Carbon $since\n * @param Carbon|null $to\n * @param string|null $crmProfileId\n *\n * @return int\n */\n public function syncLeads(Carbon $since, ?Carbon $to = null, ?string $crmProfileId = null): int\n {\n // Mark unused parameters to avoid code smell warnings\n unset($since, $to, $crmProfileId);\n\n return 0;\n }\n\n /**\n * HubSpot doesn't have leads.\n *\n * @param string $crmId\n *\n * @inheritdoc\n */\n public function syncLead(string $crmId): ?Lead\n {\n // Mark unused parameter to avoid code smell warnings\n unset($crmId);\n\n return null;\n }\n\n /**\n * Sync accounts (companies) modified since a given date (manual sync mode).\n *\n * This method fetches companies from HubSpot API based on modification date and\n * imports them one by one. It is used for:\n * - Manual sync commands (e.g., crm:sync-account with --from parameter)\n * - Initial sync for new teams\n * - Backfill operations\n *\n * For regular sync webhook batchSyncCompanies is used:\n *\n * @param Carbon $since Fetch companies modified after this date\n * @param Carbon|null $to Optional end date for modification range\n *\n * @return int Number of companies successfully synced\n */\n public function syncAccounts(Carbon $since, ?Carbon $to = null): int\n {\n $syncCount = 0;\n\n try {\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $fields = $this->getCompanyFields();\n $payload = $this->payloadBuilder->getRecentlyUpdatedSearchPayload($since, $to, $fields);\n\n foreach ($this->client->getPaginatedDataGenerator($payload, 'companies') as $hsAccount) {\n $this->importAccount($hsAccount);\n $syncCount++;\n }\n } catch (Exception $exception) {\n $this->logger->error('[' . $this->getDisplayName() . '] Sync accounts failed', [\n 'teamId' => $this->team->getUuid(),\n 'reason' => $exception->getMessage(),\n ]);\n }\n\n return $syncCount;\n }\n\n /**\n * @inheritdoc\n */\n public function syncAccount(string $crmId): ?Account\n {\n try {\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $fields = $this->getCompanyFields();\n $hsAccount = $this->client->getAccountById($crmId, $fields);\n } catch (\\HubSpot\\Client\\Crm\\Companies\\ApiException $e) {\n $this->logger->info('[' . $this->getDisplayName() . '] Account fetch failed', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n return null;\n } catch (CrmException $e) {\n $this->logger->info('[' . $this->getDisplayName() . '] Account not found', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n return null;\n }\n\n return $this->importAccount($hsAccount);\n }\n\n /**\n * Process webhook-collected contact batches.\n *\n * Drains Redis sets containing contact CRM IDs collected from webhook events\n * and dispatches ImportContactBatch jobs for batch processing.\n *\n * @return int Number of contact IDs dispatched to jobs\n */\n public function batchSyncContacts(): int\n {\n $configId = $this->team->getCrmConfiguration()->getId();\n\n return $this->batchProcessor->processBatchesForObjectType(\n WebhookSyncBatchProcessor::OBJECT_TYPE_CONTACT,\n $configId\n );\n }\n\n public function importContactBatchByIds(array $crmIds): array\n {\n $result = [\n 'success_count' => 0,\n 'failed_ids' => [],\n 'errors' => [],\n ];\n\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $batchStart = microtime(true);\n $slowContacts = [];\n\n $fetchStart = microtime(true);\n $allContacts = $this->fetchContactsByIdsInChunks($crmIds);\n $fetchMs = (int) round((microtime(true) - $fetchStart) * 1000);\n\n $fetchedIds = array_map('strval', array_column($allContacts, 'id'));\n $notFoundIds = array_values(array_diff(array_map('strval', $crmIds), $fetchedIds));\n\n if (! empty($notFoundIds)) {\n $this->logger->info('[' . $this->getDisplayName() . '] Contact CRM IDs not found in HubSpot', [\n 'teamId' => $this->team->getId(),\n 'notFoundCount' => \\count($notFoundIds),\n 'notFoundIds' => $notFoundIds,\n 'requestedCount' => \\count($crmIds),\n 'fetchedCount' => \\count($allContacts),\n ]);\n }\n\n if (empty($allContacts)) {\n return $result;\n }\n\n $prepareStart = microtime(true);\n $accountMappings = $this->prepareAccountMappingsForContacts($allContacts);\n $prepareAccountsMs = (int) round((microtime(true) - $prepareStart) * 1000);\n\n $loopStart = microtime(true);\n foreach ($allContacts as $contactData) {\n $contactStart = microtime(true);\n\n try {\n $contact = $this->importContact($contactData, $accountMappings);\n if ($contact !== null) {\n $result['success_count']++;\n }\n } catch (\\Throwable $e) {\n $crmId = $contactData['id'] ?? 'unknown';\n $result['failed_ids'][] = $crmId;\n $result['errors'][$crmId] = $e->getMessage();\n\n $this->logger->warning('[' . $this->getDisplayName() . '] Failed to import contact', [\n 'teamId' => $this->team->getId(),\n 'crmId' => $crmId,\n 'error' => $e->getMessage(),\n ]);\n }\n\n $contactMs = (int) round((microtime(true) - $contactStart) * 1000);\n if ($contactMs > 1000) {\n $slowContacts[] = ['crmId' => $contactData['id'] ?? 'unknown', 'ms' => $contactMs];\n }\n }\n $loopMs = (int) round((microtime(true) - $loopStart) * 1000);\n $totalMs = (int) round((microtime(true) - $batchStart) * 1000);\n\n $this->logger->info('[' . $this->getDisplayName() . '] importContactBatch timing', [\n 'teamId' => $this->team->getId(),\n 'contact_count' => \\count($allContacts),\n 'requested_count' => \\count($crmIds),\n 'not_found_count' => \\count($notFoundIds),\n 'total_ms' => $totalMs,\n 'fetch_api_ms' => $fetchMs,\n 'prepare_accounts_ms' => $prepareAccountsMs,\n 'contacts_loop_ms' => $loopMs,\n 'avg_contact_ms' => ! empty($allContacts) ? (int) round($loopMs / \\count($allContacts)) : 0,\n 'slow_contacts_count' => \\count($slowContacts),\n 'slow_contacts' => array_slice($slowContacts, 0, 10),\n ]);\n\n return $result;\n }\n\n private function fetchContactsByIdsInChunks(array $crmIds): array\n {\n $fields = $this->getContactFields();\n $allContacts = [];\n\n foreach (array_chunk($crmIds, self::BATCH_SIZE) as $chunk) {\n try {\n $contacts = $this->client->getContactsByIds($chunk, $fields);\n foreach ($contacts as $contactData) {\n $allContacts[] = $contactData;\n }\n } catch (\\Throwable $e) {\n // @TODO what will happen if this exception is thrown\n $this->logger->warning('[' . $this->getDisplayName() . '] Batch contact fetch failed', [\n 'chunk_size' => \\count($chunk),\n 'error' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n }\n\n return $allContacts;\n }\n\n private function prepareAccountMappingsForContacts(array $contacts): array\n {\n $companyIds = [];\n foreach ($contacts as $contact) {\n $companyId = $contact['properties']['associatedcompanyid'] ?? null;\n if ($companyId !== null && $companyId !== '') {\n $companyIds[] = (string) $companyId;\n }\n }\n\n $companyIds = array_unique($companyIds);\n\n if (empty($companyIds)) {\n return [];\n }\n\n $mappings = $this->crmEntityRepository\n ->getExistingAccountIdsMap($this->config, $companyIds);\n\n $missingCompanyIds = array_diff($companyIds, array_keys($mappings));\n\n if (empty($missingCompanyIds)) {\n return $mappings;\n }\n\n $this->logger->info('[' . $this->getDisplayName() . '] Batch syncing missing accounts for contacts', [\n 'teamId' => $this->team->getId(),\n 'total_companies' => \\count($companyIds),\n 'existing_companies' => \\count($mappings),\n 'missing_companies' => \\count($missingCompanyIds),\n ]);\n\n try {\n $syncedAccounts = $this->batchSyncAccountsForContacts($missingCompanyIds);\n $mappings = array_merge($mappings, $syncedAccounts);\n } catch (\\Throwable $e) {\n $this->logger->error('[' . $this->getDisplayName() . '] Failed to batch sync missing accounts', [\n 'teamId' => $this->team->getId(),\n 'missingCompanyIds' => $missingCompanyIds,\n 'missingCount' => count($missingCompanyIds),\n 'error' => $e->getMessage(),\n ]);\n }\n\n return $mappings;\n }\n\n private function batchSyncAccountsForContacts(array $companyIds): array\n {\n $syncedAccounts = [];\n $fields = $this->getCompanyFields();\n\n foreach (array_chunk($companyIds, self::BATCH_SIZE) as $chunk) {\n try {\n $companies = $this->client->getCompaniesByIds($chunk, $fields);\n\n foreach ($companies as $companyData) {\n try {\n $account = $this->importAccount($companyData);\n if ($account) {\n $syncedAccounts[$account->getCrmProviderId()] = $account->getId();\n }\n } catch (\\Throwable $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Failed to import account in batch', [\n 'teamId' => $this->team->getId(),\n 'companyId' => $companyData['id'] ?? 'unknown',\n 'error' => $e->getMessage(),\n ]);\n }\n }\n } catch (\\Throwable $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Batch company fetch failed', [\n 'teamId' => $this->team->getId(),\n 'chunk_size' => \\count($chunk),\n 'error' => $e->getMessage(),\n ]);\n }\n }\n\n return $syncedAccounts;\n }\n\n /**\n * Process webhook-collected company batches.\n *\n * Drains Redis sets containing company CRM IDs collected from webhook events\n * and dispatches ImportAccountBatch jobs for batch processing.\n *\n * @return int Number of company IDs dispatched to jobs\n */\n public function batchSyncCompanies(): int\n {\n $configId = $this->team->getCrmConfiguration()->getId();\n\n return $this->batchProcessor->processBatchesForObjectType(\n WebhookSyncBatchProcessor::OBJECT_TYPE_COMPANY,\n $configId\n );\n }\n\n public function importAccountBatchByIds(array $crmIds): array\n {\n $result = [\n 'success_count' => 0,\n 'failed_ids' => [],\n 'errors' => [],\n ];\n\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $batchStart = microtime(true);\n $slowAccounts = [];\n\n $fields = $this->getCompanyFields();\n $allCompanies = [];\n\n $fetchStart = microtime(true);\n foreach (array_chunk($crmIds, self::BATCH_SIZE) as $chunk) {\n try {\n $companies = $this->client->getCompaniesByIds($chunk, $fields);\n foreach ($companies as $companyData) {\n $allCompanies[] = $companyData;\n }\n } catch (\\Throwable $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Batch company fetch failed', [\n 'chunk_size' => \\count($chunk),\n 'error' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n }\n $fetchMs = (int) round((microtime(true) - $fetchStart) * 1000);\n\n $fetchedIds = array_map('strval', array_column($allCompanies, 'id'));\n $notFoundIds = array_values(array_diff(array_map('strval', $crmIds), $fetchedIds));\n\n if (! empty($notFoundIds)) {\n $this->logger->info('[' . $this->getDisplayName() . '] Company CRM IDs not found in HubSpot', [\n 'teamId' => $this->team->getId(),\n 'notFoundCount' => \\count($notFoundIds),\n 'notFoundIds' => $notFoundIds,\n 'requestedCount' => \\count($crmIds),\n 'fetchedCount' => \\count($allCompanies),\n ]);\n }\n\n $loopStart = microtime(true);\n foreach ($allCompanies as $companyData) {\n $accountStart = microtime(true);\n\n try {\n $account = $this->importAccount($companyData);\n if ($account !== null) {\n $result['success_count']++;\n }\n } catch (\\Throwable $e) {\n $crmId = $companyData['id'] ?? $companyData['properties']['hs_object_id'] ?? 'unknown';\n $result['failed_ids'][] = $crmId;\n $result['errors'][$crmId] = $e->getMessage();\n\n $this->logger->warning('[' . $this->getDisplayName() . '] Failed to import account', [\n 'crmId' => $crmId,\n 'error' => $e->getMessage(),\n ]);\n }\n\n $accountMs = (int) round((microtime(true) - $accountStart) * 1000);\n if ($accountMs > 1000) {\n $crmId = $companyData['id'] ?? $companyData['properties']['hs_object_id'] ?? 'unknown';\n $slowAccounts[] = ['crmId' => $crmId, 'ms' => $accountMs];\n }\n }\n $loopMs = (int) round((microtime(true) - $loopStart) * 1000);\n $totalMs = (int) round((microtime(true) - $batchStart) * 1000);\n\n $this->logger->info('[' . $this->getDisplayName() . '] importAccountBatch timing', [\n 'teamId' => $this->team->getId(),\n 'account_count' => \\count($allCompanies),\n 'requested_count' => \\count($crmIds),\n 'not_found_count' => \\count($notFoundIds),\n 'total_ms' => $totalMs,\n 'fetch_api_ms' => $fetchMs,\n 'accounts_loop_ms' => $loopMs,\n 'avg_account_ms' => ! empty($allCompanies) ? (int) round($loopMs / \\count($allCompanies)) : 0,\n 'slow_accounts_count' => \\count($slowAccounts),\n 'slow_accounts' => array_slice($slowAccounts, 0, 10),\n ]);\n\n return $result;\n }\n\n private function getCompanyFields(): array\n {\n return [\n 'country',\n 'name',\n 'phone',\n 'domain',\n 'hs_avatar_filemanager_key',\n 'industry',\n 'hubspot_owner_id',\n 'hs_object_id',\n 'createdate',\n 'hs_lastmodifieddate',\n ];\n }\n\n private function importAccount($crmData): ?Account\n {\n $crmProviderId = $crmData['id'] ?? $crmData['properties']['hs_object_id'] ?? null;\n\n $this->logger->info('[HubSpot] importAccount', [\n 'crm_provider_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n if (empty($crmData['properties'])) {\n $this->logger->info('[HubSpot] importAccount failed: empty properties', [\n 'crm_provider_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n return null;\n }\n\n $properties = $crmData['properties'];\n $crmId = (string) $properties['hs_object_id'];\n\n $countryCode = isset($properties['country']) ? $this->convertCountryNameToCode($properties['country']) : null;\n\n if (isset($properties['phone'])) {\n // Trim to our width and attempt to parse it.\n $number = mb_strimwidth($properties['phone'], 0, 25);\n $parsedNumber = parsePhoneNumber($countryCode, $number);\n } else {\n $parsedNumber = [];\n }\n\n $name = '[unknown]';\n if (isset($properties['name'])) {\n $name = $properties['name'];\n }\n\n $photoPath = $this->prospectPhotoPathService->getOrGeneratePhotoPath(\n $this->config,\n $crmId,\n Account::class,\n $crmId,\n $name\n );\n\n $industry = null;\n if (isset($properties['industry'])) {\n $industry = mb_strimwidth($properties['industry'], 0, 40);\n }\n\n $ownerId = $profile = null;\n if (isset($properties['hubspot_owner_id'])) {\n $ownerId = $properties['hubspot_owner_id'];\n $profile = $this->crmEntityRepository->findProfileByExternalId($this->config, (string) $ownerId);\n }\n\n $domain = null;\n if (isset($properties['domain'])) {\n $domain = StringUtil::resolveDomain($properties['domain']);\n }\n\n $remotelyCreatedAt = null;\n if (isset($properties['createdate']) && ! empty($properties['createdate'])) {\n $remotelyCreatedAt = Carbon::parse($properties['createdate']);\n }\n\n $data = [\n 'crm_provider_id' => $crmId,\n 'team_id' => $this->team->id,\n 'user_id' => $profile ? $profile->user_id : null,\n 'owner_id' => $ownerId,\n 'name' => mb_strimwidth($name, 0, 191),\n 'photo_path' => $photoPath,\n 'industry' => $industry,\n 'domain' => $domain !== null\n ? substr($domain, 0, 191)\n : null,\n 'phone' => $parsedNumber['phone'] ?? null,\n 'ext' => $parsedNumber['ext'] ?? null,\n 'country_code' => $countryCode,\n 'remotely_created_at' => $remotelyCreatedAt,\n ];\n\n return $this->crmEntityRepository->importAccount($this->config, $data);\n }\n\n public function deleteContact(string $crmProviderId): bool\n {\n try {\n $contact = $this->crmEntityRepository->findContactByExternalId($this->config, $crmProviderId);\n\n if (! $contact) {\n $this->logger->info('[HubSpot] Contact not found for deletion', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n // return success because we do not know which instance is the target\n return true;\n }\n\n $id = $contact->getId();\n\n $this->logger->info('[HubSpot] Deleting contact via webhook', [\n 'contact_id' => $id,\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n $contact->delete();\n DeleteContactJob::dispatch($id)->afterCommit();\n\n return true;\n } catch (Exception $e) {\n $this->logger->error('[HubSpot] Failed to delete contact via webhook', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n return false;\n }\n }\n\n public function deleteAccount(string $crmProviderId): bool\n {\n try {\n $account = $this->crmEntityRepository->findAccountByExternalId($this->config, $crmProviderId);\n\n if (! $account) {\n $this->logger->info('[HubSpot] Account not found for deletion', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n // return success because we do not know which instance is the target\n return true;\n }\n\n $id = $account->getId();\n\n $this->logger->info('[HubSpot] Deleting account via webhook', [\n 'account_id' => $id,\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n $account->delete();\n DeleteAccountJob::dispatch($id)->afterCommit();\n\n return true;\n } catch (Exception $e) {\n $this->logger->error('[HubSpot] Failed to delete account via webhook', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n return false;\n }\n }\n\n public function deleteOpportunity(string $crmProviderId): bool\n {\n try {\n $opportunity = $this->crmEntityRepository->findOpportunityByExternalId($this->config, $crmProviderId);\n\n if (! $opportunity) {\n $this->logger->info('[HubSpot] Opportunity not found for deletion', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n // return success because we do not know which instance is the target\n return true;\n }\n\n $id = $opportunity->getId();\n\n $this->logger->info('[HubSpot] Deleting opportunity via webhook', [\n 'opportunity_id' => $id,\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n $opportunity->delete();\n DeleteOpportunityJob::dispatch($id)->afterCommit();\n\n return true;\n } catch (Exception $e) {\n $this->logger->error('[HubSpot] Failed to delete opportunity via webhook', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n return false;\n }\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits;\n\nuse Carbon\\Carbon;\nuse Exception;\nuse Illuminate\\Support\\Str;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Jobs\\Crm\\Delete\\DeleteAccountJob;\nuse Jiminny\\Jobs\\Crm\\Delete\\DeleteContactJob;\nuse Jiminny\\Jobs\\Crm\\Delete\\DeleteOpportunityJob;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Repositories\\Crm\\CrmEntityRepository;\nuse Jiminny\\Services\\Avatar\\ProspectPhotoPathService;\nuse Jiminny\\Services\\Crm\\Hubspot\\HubspotClientInterface;\nuse Jiminny\\Services\\Crm\\Hubspot\\WebhookSyncBatchProcessor;\nuse Jiminny\\Utils\\StringUtil;\n\ntrait SyncCrmEntitiesTrait\n{\n use OpportunitySyncTrait;\n private const string CDN_URL = 'https://cdn2.hubspot.net/';\n protected CrmEntityRepository $crmEntityRepository;\n protected ProspectPhotoPathService $prospectPhotoPathService;\n\n private function getAssociationDataForCollection(array $collection, string $fromObject, string $toObject): array\n {\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $hsOpportunityIds = array_column($collection, 'id');\n\n return $this->client->getAssociationsData($hsOpportunityIds, $fromObject, $toObject);\n }\n\n private function importAssociationData(array $collection, array $associatedData): array\n {\n $data = [];\n if (! empty($associatedData[$collection['id']])) {\n foreach ($associatedData[$collection['id']] as $id) {\n $data[] = [\n 'id' => $id,\n ];\n }\n }\n\n return ['results' => $data];\n }\n\n /**\n * Sync contacts modified since a given date (manual sync mode).\n *\n * This method fetches contacts from HubSpot API based on modification date and\n * imports them one by one. It is used for:\n * - Manual sync commands (e.g., crm:sync-contact with --from parameter)\n * - Initial sync for new teams\n * - Backfill operations\n *\n * For regular sync webhook batchSyncContacts is used:\n *\n * @param Carbon $since Fetch contacts modified after this date\n * @param Carbon|null $to Optional end date for modification range\n *\n * @return int Number of contacts successfully synced\n */\n public function syncContacts(Carbon $since, ?Carbon $to = null): int\n {\n $syncCount = 0;\n\n try {\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $fields = $this->getContactFields();\n $payload = $this->payloadBuilder->getRecentlyUpdatedSearchPayload($since, $to, $fields);\n\n foreach ($this->client->getPaginatedDataGenerator($payload, 'contacts') as $hsContact) {\n $this->importContact($hsContact);\n $syncCount++;\n }\n } catch (Exception $exception) {\n $this->logger->error('[' . $this->getDisplayName() . '] Sync contacts failed', [\n 'teamId' => $this->team->getUuid(),\n 'reason' => $exception->getMessage(),\n ]);\n }\n\n return $syncCount;\n }\n\n /**\n * @inheritdoc\n */\n public function syncContact(string $crmId): ?Contact\n {\n try {\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $fields = $this->getContactFields();\n $hsContact = $this->client->getContactById($crmId, $fields);\n } catch (\\HubSpot\\Client\\Crm\\Contacts\\ApiException $e) {\n $this->logger->info('[' . $this->getDisplayName() . '] Contact fetch failed', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n return null;\n } catch (CrmException $e) {\n $this->logger->info('[' . $this->getDisplayName() . '] Contact not found', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n return null;\n }\n\n if (empty($hsContact['properties']) || empty($hsContact['id'])) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Contact data incomplete', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'has_properties' => ! empty($hsContact['properties']),\n 'has_id' => ! empty($hsContact['id']),\n ]);\n\n return null;\n }\n\n return $this->importContact($hsContact);\n }\n\n private function getContactFields(): array\n {\n return [\n 'associatedcompanyid',\n 'country',\n 'firstname',\n 'lastname',\n 'phone',\n 'mobilephone',\n 'email',\n 'photo',\n 'hs_avatar_filemanager_key',\n 'jobtitle',\n 'hubspot_owner_id',\n 'hs_object_id',\n 'createdate',\n 'hs_lastmodifieddate',\n ];\n }\n\n /**\n * @inheritdoc\n */\n private function importContact($crmData, array $accountMappings = []): ?Contact\n {\n $crmProviderId = $crmData['id'] ?? null;\n\n $this->logger->info('[HubSpot] importContact', [\n 'crm_provider_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n if (empty($crmData['properties'])) {\n $this->logger->info('[HubSpot] importContact failed: empty properties', [\n 'crm_provider_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n return null;\n }\n\n $properties = $crmData['properties'];\n $crmId = (string) $crmData['id'];\n\n $accountId = $this->resolveContactAccount($properties, $accountMappings);\n $data = $this->buildContactData($crmId, $properties, $accountId);\n\n return $this->crmEntityRepository->importContact($this->config, $data);\n }\n\n private function resolveContactAccount(array $properties, array $accountMappings): ?int\n {\n if (empty($properties['associatedcompanyid'])) {\n return null;\n }\n\n $companyId = (string) $properties['associatedcompanyid'];\n\n if (! empty($accountMappings)) {\n return $accountMappings[$companyId] ?? null;\n }\n\n return $this->crmEntityRepository->findAccountByExternalId(\n $this->team->getCrmConfiguration(),\n $companyId\n )?->getId() ?? $this->syncAccount($companyId)?->getId();\n }\n\n private function buildContactData(string $crmId, array $properties, ?int $accountId): array\n {\n $countryCode = $this->buildContactCountry($properties);\n $name = $this->buildContactName($properties);\n $photoPath = $this->teamService->generateAvatar(\n $crmId,\n empty($name) ? ($properties['email'] ?? 'N/A') : $name,\n );\n $parsedNumber = $this->buildContactPhone($countryCode, $properties);\n $mobileNumber = $this->buildContactMobilePhone($countryCode, $properties);\n\n $ownerId = $properties['hubspot_owner_id'] ?? null;\n $profile = $ownerId !== null\n ? $this->crmEntityRepository->findProfileByExternalId($this->config, (string) $ownerId)\n : null;\n\n $ext = (isset($parsedNumber['ext']) && is_string($parsedNumber['ext']) && strlen($parsedNumber['ext']) <= 10)\n ? $parsedNumber['ext']\n : null;\n\n $title = isset($properties['jobtitle']) ? mb_strimwidth($properties['jobtitle'], 0, 128) : null;\n $email = isset($properties['email']) ? mb_strimwidth($properties['email'], 0, 191) : null;\n $remotelyCreatedAt = ! empty($properties['createdate']) ? Carbon::parse($properties['createdate']) : null;\n\n return [\n 'crm_provider_id' => $crmId,\n 'team_id' => $this->team->getId(),\n 'account_id' => $accountId,\n 'user_id' => $profile?->getUserId(),\n 'owner_id' => $ownerId,\n 'name' => $name,\n 'title' => $title,\n 'email' => $email,\n 'country_code' => $countryCode,\n 'phone' => $parsedNumber['phone'] ?? null,\n 'mobile_phone' => $mobileNumber ?? null,\n 'ext' => $ext,\n 'photo_path' => $photoPath,\n 'remotely_created_at' => $remotelyCreatedAt,\n ];\n }\n\n /**\n * @param $properties\n */\n private function buildContactName($properties): string\n {\n if (is_array($properties)) {\n return $this->buildContactNameFromArray($properties);\n }\n\n return $this->buildContactNameFromObject($properties);\n }\n\n private function buildContactNameFromArray(array $properties): string\n {\n if (! empty($properties['name'])) {\n return mb_strimwidth($properties['name'], 0, 100);\n }\n\n $name = '';\n if (! empty($properties['firstname'])) {\n $name = $properties['firstname'] . ' ';\n }\n\n if (! empty($properties['lastname'])) {\n $name .= $properties['lastname'];\n }\n\n if ($name === '' && ! empty($properties['email'])) {\n $name = $properties['email'];\n }\n\n return mb_strimwidth($name, 0, 100);\n }\n\n private function buildContactNameFromObject($properties): string\n {\n $name = '';\n if (isset($properties->firstname)) {\n $name = $properties->firstname->value . ' ';\n }\n if (isset($properties->lastname)) {\n $name .= $properties->lastname->value;\n }\n if ($name === '' && isset($properties->email)) {\n $name = $properties->email->value;\n }\n\n return mb_strimwidth($name, 0, 100);\n }\n\n /**\n * @param $properties\n */\n private function buildContactPhone(?string $countryCode, $properties): ?array\n {\n if (is_array($properties) && empty($properties['phone']) === false) {\n $number = mb_strimwidth($properties['phone'], 0, 25);\n\n return parsePhoneNumber($countryCode, $number);\n } elseif (isset($properties->phone)) {\n $number = mb_strimwidth($properties->phone->value, 0, 25);\n\n return parsePhoneNumber($countryCode, $number);\n }\n\n return [];\n }\n\n /**\n * @param $properties\n */\n private function buildContactMobilePhone(?string $countryCode, $properties): ?string\n {\n return isset($properties['mobilephone'])\n ? Str::limit(phone_e164($countryCode, $properties['mobilephone']), 25, '')\n : null;\n }\n\n /**\n * @param $properties\n * @param $account\n */\n private function buildContactCountry($properties): ?string\n {\n if (is_array($properties) && empty($properties['country']) === false) {\n return $this->convertCountryNameToCode($properties['country']);\n }\n\n if (isset($properties->country)) {\n return $this->convertCountryNameToCode($properties->country->value);\n }\n\n return null;\n }\n\n /**\n * HubSpot doesn't have leads, so this method does nothing.\n *\n * @param Carbon $since\n * @param Carbon|null $to\n * @param string|null $crmProfileId\n *\n * @return int\n */\n public function syncLeads(Carbon $since, ?Carbon $to = null, ?string $crmProfileId = null): int\n {\n // Mark unused parameters to avoid code smell warnings\n unset($since, $to, $crmProfileId);\n\n return 0;\n }\n\n /**\n * HubSpot doesn't have leads.\n *\n * @param string $crmId\n *\n * @inheritdoc\n */\n public function syncLead(string $crmId): ?Lead\n {\n // Mark unused parameter to avoid code smell warnings\n unset($crmId);\n\n return null;\n }\n\n /**\n * Sync accounts (companies) modified since a given date (manual sync mode).\n *\n * This method fetches companies from HubSpot API based on modification date and\n * imports them one by one. It is used for:\n * - Manual sync commands (e.g., crm:sync-account with --from parameter)\n * - Initial sync for new teams\n * - Backfill operations\n *\n * For regular sync webhook batchSyncCompanies is used:\n *\n * @param Carbon $since Fetch companies modified after this date\n * @param Carbon|null $to Optional end date for modification range\n *\n * @return int Number of companies successfully synced\n */\n public function syncAccounts(Carbon $since, ?Carbon $to = null): int\n {\n $syncCount = 0;\n\n try {\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $fields = $this->getCompanyFields();\n $payload = $this->payloadBuilder->getRecentlyUpdatedSearchPayload($since, $to, $fields);\n\n foreach ($this->client->getPaginatedDataGenerator($payload, 'companies') as $hsAccount) {\n $this->importAccount($hsAccount);\n $syncCount++;\n }\n } catch (Exception $exception) {\n $this->logger->error('[' . $this->getDisplayName() . '] Sync accounts failed', [\n 'teamId' => $this->team->getUuid(),\n 'reason' => $exception->getMessage(),\n ]);\n }\n\n return $syncCount;\n }\n\n /**\n * @inheritdoc\n */\n public function syncAccount(string $crmId): ?Account\n {\n try {\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $fields = $this->getCompanyFields();\n $hsAccount = $this->client->getAccountById($crmId, $fields);\n } catch (\\HubSpot\\Client\\Crm\\Companies\\ApiException $e) {\n $this->logger->info('[' . $this->getDisplayName() . '] Account fetch failed', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n return null;\n } catch (CrmException $e) {\n $this->logger->info('[' . $this->getDisplayName() . '] Account not found', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n return null;\n }\n\n return $this->importAccount($hsAccount);\n }\n\n /**\n * Process webhook-collected contact batches.\n *\n * Drains Redis sets containing contact CRM IDs collected from webhook events\n * and dispatches ImportContactBatch jobs for batch processing.\n *\n * @return int Number of contact IDs dispatched to jobs\n */\n public function batchSyncContacts(): int\n {\n $configId = $this->team->getCrmConfiguration()->getId();\n\n return $this->batchProcessor->processBatchesForObjectType(\n WebhookSyncBatchProcessor::OBJECT_TYPE_CONTACT,\n $configId\n );\n }\n\n public function importContactBatchByIds(array $crmIds): array\n {\n $result = [\n 'success_count' => 0,\n 'failed_ids' => [],\n 'errors' => [],\n ];\n\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $batchStart = microtime(true);\n $slowContacts = [];\n\n $fetchStart = microtime(true);\n $allContacts = $this->fetchContactsByIdsInChunks($crmIds);\n $fetchMs = (int) round((microtime(true) - $fetchStart) * 1000);\n\n $fetchedIds = array_map('strval', array_column($allContacts, 'id'));\n $notFoundIds = array_values(array_diff(array_map('strval', $crmIds), $fetchedIds));\n\n if (! empty($notFoundIds)) {\n $this->logger->info('[' . $this->getDisplayName() . '] Contact CRM IDs not found in HubSpot', [\n 'teamId' => $this->team->getId(),\n 'notFoundCount' => \\count($notFoundIds),\n 'notFoundIds' => $notFoundIds,\n 'requestedCount' => \\count($crmIds),\n 'fetchedCount' => \\count($allContacts),\n ]);\n }\n\n if (empty($allContacts)) {\n return $result;\n }\n\n $prepareStart = microtime(true);\n $accountMappings = $this->prepareAccountMappingsForContacts($allContacts);\n $prepareAccountsMs = (int) round((microtime(true) - $prepareStart) * 1000);\n\n $loopStart = microtime(true);\n foreach ($allContacts as $contactData) {\n $contactStart = microtime(true);\n\n try {\n $contact = $this->importContact($contactData, $accountMappings);\n if ($contact !== null) {\n $result['success_count']++;\n }\n } catch (\\Throwable $e) {\n $crmId = $contactData['id'] ?? 'unknown';\n $result['failed_ids'][] = $crmId;\n $result['errors'][$crmId] = $e->getMessage();\n\n $this->logger->warning('[' . $this->getDisplayName() . '] Failed to import contact', [\n 'teamId' => $this->team->getId(),\n 'crmId' => $crmId,\n 'error' => $e->getMessage(),\n ]);\n }\n\n $contactMs = (int) round((microtime(true) - $contactStart) * 1000);\n if ($contactMs > 1000) {\n $slowContacts[] = ['crmId' => $contactData['id'] ?? 'unknown', 'ms' => $contactMs];\n }\n }\n $loopMs = (int) round((microtime(true) - $loopStart) * 1000);\n $totalMs = (int) round((microtime(true) - $batchStart) * 1000);\n\n $this->logger->info('[' . $this->getDisplayName() . '] importContactBatch timing', [\n 'teamId' => $this->team->getId(),\n 'contact_count' => \\count($allContacts),\n 'requested_count' => \\count($crmIds),\n 'not_found_count' => \\count($notFoundIds),\n 'total_ms' => $totalMs,\n 'fetch_api_ms' => $fetchMs,\n 'prepare_accounts_ms' => $prepareAccountsMs,\n 'contacts_loop_ms' => $loopMs,\n 'avg_contact_ms' => ! empty($allContacts) ? (int) round($loopMs / \\count($allContacts)) : 0,\n 'slow_contacts_count' => \\count($slowContacts),\n 'slow_contacts' => array_slice($slowContacts, 0, 10),\n ]);\n\n return $result;\n }\n\n private function fetchContactsByIdsInChunks(array $crmIds): array\n {\n $fields = $this->getContactFields();\n $allContacts = [];\n\n foreach (array_chunk($crmIds, self::BATCH_SIZE) as $chunk) {\n try {\n $contacts = $this->client->getContactsByIds($chunk, $fields);\n foreach ($contacts as $contactData) {\n $allContacts[] = $contactData;\n }\n } catch (\\Throwable $e) {\n // @TODO what will happen if this exception is thrown\n $this->logger->warning('[' . $this->getDisplayName() . '] Batch contact fetch failed', [\n 'chunk_size' => \\count($chunk),\n 'error' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n }\n\n return $allContacts;\n }\n\n private function prepareAccountMappingsForContacts(array $contacts): array\n {\n $companyIds = [];\n foreach ($contacts as $contact) {\n $companyId = $contact['properties']['associatedcompanyid'] ?? null;\n if ($companyId !== null && $companyId !== '') {\n $companyIds[] = (string) $companyId;\n }\n }\n\n $companyIds = array_unique($companyIds);\n\n if (empty($companyIds)) {\n return [];\n }\n\n $mappings = $this->crmEntityRepository\n ->getExistingAccountIdsMap($this->config, $companyIds);\n\n $missingCompanyIds = array_diff($companyIds, array_keys($mappings));\n\n if (empty($missingCompanyIds)) {\n return $mappings;\n }\n\n $this->logger->info('[' . $this->getDisplayName() . '] Batch syncing missing accounts for contacts', [\n 'teamId' => $this->team->getId(),\n 'total_companies' => \\count($companyIds),\n 'existing_companies' => \\count($mappings),\n 'missing_companies' => \\count($missingCompanyIds),\n ]);\n\n try {\n $syncedAccounts = $this->batchSyncAccountsForContacts($missingCompanyIds);\n $mappings = array_merge($mappings, $syncedAccounts);\n } catch (\\Throwable $e) {\n $this->logger->error('[' . $this->getDisplayName() . '] Failed to batch sync missing accounts', [\n 'teamId' => $this->team->getId(),\n 'missingCompanyIds' => $missingCompanyIds,\n 'missingCount' => count($missingCompanyIds),\n 'error' => $e->getMessage(),\n ]);\n }\n\n return $mappings;\n }\n\n private function batchSyncAccountsForContacts(array $companyIds): array\n {\n $syncedAccounts = [];\n $fields = $this->getCompanyFields();\n\n foreach (array_chunk($companyIds, self::BATCH_SIZE) as $chunk) {\n try {\n $companies = $this->client->getCompaniesByIds($chunk, $fields);\n\n foreach ($companies as $companyData) {\n try {\n $account = $this->importAccount($companyData);\n if ($account) {\n $syncedAccounts[$account->getCrmProviderId()] = $account->getId();\n }\n } catch (\\Throwable $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Failed to import account in batch', [\n 'teamId' => $this->team->getId(),\n 'companyId' => $companyData['id'] ?? 'unknown',\n 'error' => $e->getMessage(),\n ]);\n }\n }\n } catch (\\Throwable $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Batch company fetch failed', [\n 'teamId' => $this->team->getId(),\n 'chunk_size' => \\count($chunk),\n 'error' => $e->getMessage(),\n ]);\n }\n }\n\n return $syncedAccounts;\n }\n\n /**\n * Process webhook-collected company batches.\n *\n * Drains Redis sets containing company CRM IDs collected from webhook events\n * and dispatches ImportAccountBatch jobs for batch processing.\n *\n * @return int Number of company IDs dispatched to jobs\n */\n public function batchSyncCompanies(): int\n {\n $configId = $this->team->getCrmConfiguration()->getId();\n\n return $this->batchProcessor->processBatchesForObjectType(\n WebhookSyncBatchProcessor::OBJECT_TYPE_COMPANY,\n $configId\n );\n }\n\n public function importAccountBatchByIds(array $crmIds): array\n {\n $result = [\n 'success_count' => 0,\n 'failed_ids' => [],\n 'errors' => [],\n ];\n\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $batchStart = microtime(true);\n $slowAccounts = [];\n\n $fields = $this->getCompanyFields();\n $allCompanies = [];\n\n $fetchStart = microtime(true);\n foreach (array_chunk($crmIds, self::BATCH_SIZE) as $chunk) {\n try {\n $companies = $this->client->getCompaniesByIds($chunk, $fields);\n foreach ($companies as $companyData) {\n $allCompanies[] = $companyData;\n }\n } catch (\\Throwable $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Batch company fetch failed', [\n 'chunk_size' => \\count($chunk),\n 'error' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n }\n $fetchMs = (int) round((microtime(true) - $fetchStart) * 1000);\n\n $fetchedIds = array_map('strval', array_column($allCompanies, 'id'));\n $notFoundIds = array_values(array_diff(array_map('strval', $crmIds), $fetchedIds));\n\n if (! empty($notFoundIds)) {\n $this->logger->info('[' . $this->getDisplayName() . '] Company CRM IDs not found in HubSpot', [\n 'teamId' => $this->team->getId(),\n 'notFoundCount' => \\count($notFoundIds),\n 'notFoundIds' => $notFoundIds,\n 'requestedCount' => \\count($crmIds),\n 'fetchedCount' => \\count($allCompanies),\n ]);\n }\n\n $loopStart = microtime(true);\n foreach ($allCompanies as $companyData) {\n $accountStart = microtime(true);\n\n try {\n $account = $this->importAccount($companyData);\n if ($account !== null) {\n $result['success_count']++;\n }\n } catch (\\Throwable $e) {\n $crmId = $companyData['id'] ?? $companyData['properties']['hs_object_id'] ?? 'unknown';\n $result['failed_ids'][] = $crmId;\n $result['errors'][$crmId] = $e->getMessage();\n\n $this->logger->warning('[' . $this->getDisplayName() . '] Failed to import account', [\n 'crmId' => $crmId,\n 'error' => $e->getMessage(),\n ]);\n }\n\n $accountMs = (int) round((microtime(true) - $accountStart) * 1000);\n if ($accountMs > 1000) {\n $crmId = $companyData['id'] ?? $companyData['properties']['hs_object_id'] ?? 'unknown';\n $slowAccounts[] = ['crmId' => $crmId, 'ms' => $accountMs];\n }\n }\n $loopMs = (int) round((microtime(true) - $loopStart) * 1000);\n $totalMs = (int) round((microtime(true) - $batchStart) * 1000);\n\n $this->logger->info('[' . $this->getDisplayName() . '] importAccountBatch timing', [\n 'teamId' => $this->team->getId(),\n 'account_count' => \\count($allCompanies),\n 'requested_count' => \\count($crmIds),\n 'not_found_count' => \\count($notFoundIds),\n 'total_ms' => $totalMs,\n 'fetch_api_ms' => $fetchMs,\n 'accounts_loop_ms' => $loopMs,\n 'avg_account_ms' => ! empty($allCompanies) ? (int) round($loopMs / \\count($allCompanies)) : 0,\n 'slow_accounts_count' => \\count($slowAccounts),\n 'slow_accounts' => array_slice($slowAccounts, 0, 10),\n ]);\n\n return $result;\n }\n\n private function getCompanyFields(): array\n {\n return [\n 'country',\n 'name',\n 'phone',\n 'domain',\n 'hs_avatar_filemanager_key',\n 'industry',\n 'hubspot_owner_id',\n 'hs_object_id',\n 'createdate',\n 'hs_lastmodifieddate',\n ];\n }\n\n private function importAccount($crmData): ?Account\n {\n $crmProviderId = $crmData['id'] ?? $crmData['properties']['hs_object_id'] ?? null;\n\n $this->logger->info('[HubSpot] importAccount', [\n 'crm_provider_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n if (empty($crmData['properties'])) {\n $this->logger->info('[HubSpot] importAccount failed: empty properties', [\n 'crm_provider_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n return null;\n }\n\n $properties = $crmData['properties'];\n $crmId = (string) $properties['hs_object_id'];\n\n $countryCode = isset($properties['country']) ? $this->convertCountryNameToCode($properties['country']) : null;\n\n if (isset($properties['phone'])) {\n // Trim to our width and attempt to parse it.\n $number = mb_strimwidth($properties['phone'], 0, 25);\n $parsedNumber = parsePhoneNumber($countryCode, $number);\n } else {\n $parsedNumber = [];\n }\n\n $name = '[unknown]';\n if (isset($properties['name'])) {\n $name = $properties['name'];\n }\n\n $photoPath = $this->prospectPhotoPathService->getOrGeneratePhotoPath(\n $this->config,\n $crmId,\n Account::class,\n $crmId,\n $name\n );\n\n $industry = null;\n if (isset($properties['industry'])) {\n $industry = mb_strimwidth($properties['industry'], 0, 40);\n }\n\n $ownerId = $profile = null;\n if (isset($properties['hubspot_owner_id'])) {\n $ownerId = $properties['hubspot_owner_id'];\n $profile = $this->crmEntityRepository->findProfileByExternalId($this->config, (string) $ownerId);\n }\n\n $domain = null;\n if (isset($properties['domain'])) {\n $domain = StringUtil::resolveDomain($properties['domain']);\n }\n\n $remotelyCreatedAt = null;\n if (isset($properties['createdate']) && ! empty($properties['createdate'])) {\n $remotelyCreatedAt = Carbon::parse($properties['createdate']);\n }\n\n $data = [\n 'crm_provider_id' => $crmId,\n 'team_id' => $this->team->id,\n 'user_id' => $profile ? $profile->user_id : null,\n 'owner_id' => $ownerId,\n 'name' => mb_strimwidth($name, 0, 191),\n 'photo_path' => $photoPath,\n 'industry' => $industry,\n 'domain' => $domain !== null\n ? substr($domain, 0, 191)\n : null,\n 'phone' => $parsedNumber['phone'] ?? null,\n 'ext' => $parsedNumber['ext'] ?? null,\n 'country_code' => $countryCode,\n 'remotely_created_at' => $remotelyCreatedAt,\n ];\n\n return $this->crmEntityRepository->importAccount($this->config, $data);\n }\n\n public function deleteContact(string $crmProviderId): bool\n {\n try {\n $contact = $this->crmEntityRepository->findContactByExternalId($this->config, $crmProviderId);\n\n if (! $contact) {\n $this->logger->info('[HubSpot] Contact not found for deletion', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n // return success because we do not know which instance is the target\n return true;\n }\n\n $id = $contact->getId();\n\n $this->logger->info('[HubSpot] Deleting contact via webhook', [\n 'contact_id' => $id,\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n $contact->delete();\n DeleteContactJob::dispatch($id)->afterCommit();\n\n return true;\n } catch (Exception $e) {\n $this->logger->error('[HubSpot] Failed to delete contact via webhook', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n return false;\n }\n }\n\n public function deleteAccount(string $crmProviderId): bool\n {\n try {\n $account = $this->crmEntityRepository->findAccountByExternalId($this->config, $crmProviderId);\n\n if (! $account) {\n $this->logger->info('[HubSpot] Account not found for deletion', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n // return success because we do not know which instance is the target\n return true;\n }\n\n $id = $account->getId();\n\n $this->logger->info('[HubSpot] Deleting account via webhook', [\n 'account_id' => $id,\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n $account->delete();\n DeleteAccountJob::dispatch($id)->afterCommit();\n\n return true;\n } catch (Exception $e) {\n $this->logger->error('[HubSpot] Failed to delete account via webhook', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n return false;\n }\n }\n\n public function deleteOpportunity(string $crmProviderId): bool\n {\n try {\n $opportunity = $this->crmEntityRepository->findOpportunityByExternalId($this->config, $crmProviderId);\n\n if (! $opportunity) {\n $this->logger->info('[HubSpot] Opportunity not found for deletion', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n // return success because we do not know which instance is the target\n return true;\n }\n\n $id = $opportunity->getId();\n\n $this->logger->info('[HubSpot] Deleting opportunity via webhook', [\n 'opportunity_id' => $id,\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n $opportunity->delete();\n DeleteOpportunityJob::dispatch($id)->afterCommit();\n\n return true;\n } catch (Exception $e) {\n $this->logger->error('[HubSpot] Failed to delete opportunity via webhook', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n return false;\n }\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"app ~/jiminny/app, folder","depth":6,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".circleci, folder","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".cursor, folder","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".github","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".sonarlint, folder","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".vscode, folder","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":".windsurf, folder","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"app, sources root","depth":7,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Actions, folder","depth":8,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Component, folder","depth":8,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Acl, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ActionItems, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Activity, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ActivityAnalytics, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ActivitySearch, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"AiActivityType, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"AiAutomation, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"AiCallScoring, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"AskAnything, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Dtos, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Events, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"AskAnythingPromptService.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"HistoryService.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"AskJiminnyAi, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"AWS, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"BillingManagement, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Cache, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"CoachingFeedback, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Country, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"CustomerApi, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Database, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Datadog, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"DateTime, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"DealInsights, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"DealRisks, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ElasticSearch","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Eloquent","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Encoding","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Encryption","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ES","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Faker","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"FeatureFlags","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"FFMpeg","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"FileSystem","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Gecko","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Gong","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"GuzzleHttp","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"KeyPoints","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Kiosk","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"LanguageDetection","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"LiveFeed","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Locks","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Math","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"MediaPipeline","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"MeetingBot, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"MobileSettings, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Model, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Notification, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Nudge, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ParagraphBreaker, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ParticipantSpeech, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"PartitionedCookie, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"PlaybackPage, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Playlist, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Prophet, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ProphetAi, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ProsperWorks, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Queue, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Job, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"RateLimitAware.php, abstract class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"RateLimitAwareWrapper.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"BotsQueueConstants.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Constants.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ProcessingQueueConstants.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Router, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Saml2, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"SCIM, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Seeder, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Sentry, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Serializer, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Settings, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Sidekick, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Slack, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"TeamInsights, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"TimeMemoryMapper, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Transcription, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"TranscriptionSummary, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Twilio, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Uploader, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"UrlGenerator, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Utility, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Exceptions, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Service, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"BaseRateLimiter.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"EfficientJsonParser.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ProviderRateLimiter.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"RateLimiterInstance.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Uuid, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Waveform, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Webhooks, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Workflow, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Configuration, folder","depth":8,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Console, folder","depth":8,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Commands, folder","depth":9,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Activities, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Analytics, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Calendars, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Crm, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Hubspot, folder","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"IntegrationApp, folder","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Traits, folder","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"AddLayoutEntities.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"AutologDelayedCommand.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"BullhornCommandAbstract.php, abstract class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"BullhornPingCommand.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"BullhornSearchCommand.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"BullhornSessionCommand.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"CheckActivityLoggableCommand.php, final class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"CleanDuplicateFieldDataCommand.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"FullSyncOpportunityCommand.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"LogActivitiesCommand.php, final class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ManageSyncStrategyCommand.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"MatchCrmObjectsCommand.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"MatchOpportunityActivitiesCommand.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"MigrateProvider.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ProcessHubspotObjectsSyncBatches.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"PurgeDeletedOpportunitiesCommand.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ResetGovernorLimits.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"SendNotLogged.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"SetupActivityTypeForFollowUp.php, final class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"SetupCloseCrm.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"SetupCopperCrm.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"SetupCrmCommand.php, abstract class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"SetupLayouts.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"SyncAccount.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"SyncContact.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"SyncFieldMetadata.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"SyncHubspotActiveDeals.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"SyncHubspotObjects.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"SyncLead.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"SyncObjects.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"SyncOpportunitiesMissingFieldDataCommand.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"SyncOpportunity.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"SyncProfileMetadata.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"SyncTeamMetadata.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"UpdateOpportunitySpecifications.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"DealInsights, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Dev, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"AddRateLimitCommand.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"FixHubSpotTokens.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"FixMissMatchedCrmActivitiesCommand.php, final class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ImportCallsCommand.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"MonitorSocialAccountsState.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Dialers, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"DTOs, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Elasticsearch, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"EngagementStats, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"GeckoExport, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Livestream, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Mailboxes, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Migrate, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"PlaybackThemes, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Playbooks, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Playlists, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Postmark, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ProphetAi, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Reports, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"AutomatedReportsCommand.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"AutomatedReportsRetentionPolicyCommand.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"AutomatedReportsSendCommand.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"CreateMockAskJiminnyReportResultCommand.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"DeleteReportCommand.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"GenerateMarketingReport.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Team.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Usage.php, class","depth":11,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Slack, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Teams, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Tracks, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Transcription, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Twilio, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Users, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Vocabulary, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Zoom, folder","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"Command.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"CreateDatabaseUsers.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"DatabaseTableCount.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"DeleteOldAiCrmNotesCommand.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"DeleteS3LeftoversCommand.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"DevPostmanCommand.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"DiarizeViaAiParticipantIdentificationCommand.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"EncryptTokensCommand.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"EngagementStatsRegenerateCommand.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"FeatureFlagsHelper.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"FixCrossTenantIssues.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"FlushRolesPermissionsCache.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"GenerateInternalWebhookToken.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"GroupSetDefaultLanguageCommand.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"HelperTruncateCoachingTables.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"HubspotJournalPollingCommand.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"HubspotWebhookServiceCommand.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ImportRecording.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ImportUsersFromCsvFile.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"IterateUsersCommand.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"JiminnyCacheClearCommand.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"JiminnyDebugCommand.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"JiminnySetEncryptedTokenManagerModeCommand.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"JiminnyTokenInfoCommand.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"MakeSlackLiveCoachingChatNotesOn.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"ManageScimForTeam.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"MarkBranchForEnvironmentPipelineCommand.php","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"MuteOrganizerChannel.php, class","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"PhpApm.php, class","depth":10,"on_screen":false,"role_description":"text"},{"role":"AXStaticText","text":"PropagateCoachingFeedbackCreatedAtToSectionFeedbacks.php, class","depth":10,"on_screen":false,"role_description":"text"}]...
|
-5207249351467171980
|
5036074921934260454
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
Editor for custom.log
Sync Changes
Hide This Notification
Code changed:
Hide
Analyzing…
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\ServiceTraits;
use Carbon\Carbon;
use Exception;
use Illuminate\Support\Str;
use Jiminny\Exceptions\CrmException;
use Jiminny\Jobs\Crm\Delete\DeleteAccountJob;
use Jiminny\Jobs\Crm\Delete\DeleteContactJob;
use Jiminny\Jobs\Crm\Delete\DeleteOpportunityJob;
use Jiminny\Models\Account;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Repositories\Crm\CrmEntityRepository;
use Jiminny\Services\Avatar\ProspectPhotoPathService;
use Jiminny\Services\Crm\Hubspot\HubspotClientInterface;
use Jiminny\Services\Crm\Hubspot\WebhookSyncBatchProcessor;
use Jiminny\Utils\StringUtil;
trait SyncCrmEntitiesTrait
{
use OpportunitySyncTrait;
private const string CDN_URL = '[URL_WITH_CREDENTIALS] Carbon $since Fetch contacts modified after this date
* @param Carbon|null $to Optional end date for modification range
*
* @return int Number of contacts successfully synced
*/
public function syncContacts(Carbon $since, ?Carbon $to = null): int
{
$syncCount = 0;
try {
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$fields = $this->getContactFields();
$payload = $this->payloadBuilder->getRecentlyUpdatedSearchPayload($since, $to, $fields);
foreach ($this->client->getPaginatedDataGenerator($payload, 'contacts') as $hsContact) {
$this->importContact($hsContact);
$syncCount++;
}
} catch (Exception $exception) {
$this->logger->error('[' . $this->getDisplayName() . '] Sync contacts failed', [
'teamId' => $this->team->getUuid(),
'reason' => $exception->getMessage(),
]);
}
return $syncCount;
}
/**
* @inheritdoc
*/
public function syncContact(string $crmId): ?Contact
{
try {
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$fields = $this->getContactFields();
$hsContact = $this->client->getContactById($crmId, $fields);
} catch (\HubSpot\Client\Crm\Contacts\ApiException $e) {
$this->logger->info('[' . $this->getDisplayName() . '] Contact fetch failed', [
'teamId' => $this->team->getUuid(),
'crmId' => $crmId,
'reason' => $e->getMessage(),
]);
return null;
} catch (CrmException $e) {
$this->logger->info('[' . $this->getDisplayName() . '] Contact not found', [
'teamId' => $this->team->getUuid(),
'crmId' => $crmId,
'reason' => $e->getMessage(),
]);
return null;
}
if (empty($hsContact['properties']) || empty($hsContact['id'])) {
$this->logger->warning('[' . $this->getDisplayName() . '] Contact data incomplete', [
'teamId' => $this->team->getUuid(),
'crmId' => $crmId,
'has_properties' => ! empty($hsContact['properties']),
'has_id' => ! empty($hsContact['id']),
]);
return null;
}
return $this->importContact($hsContact);
}
private function getContactFields(): array
{
return [
'associatedcompanyid',
'country',
'firstname',
'lastname',
'phone',
'mobilephone',
'email',
'photo',
'hs_avatar_filemanager_key',
'jobtitle',
'hubspot_owner_id',
'hs_object_id',
'createdate',
'hs_lastmodifieddate',
];
}
/**
* @inheritdoc
*/
private function importContact($crmData, array $accountMappings = []): ?Contact
{
$crmProviderId = $crmData['id'] ?? null;
$this->logger->info('[HubSpot] importContact', [
'crm_provider_id' => $crmProviderId,
'config_id' => $this->config->getId(),
]);
if (empty($crmData['properties'])) {
$this->logger->info('[HubSpot] importContact failed: empty properties', [
'crm_provider_id' => $crmProviderId,
'config_id' => $this->config->getId(),
]);
return null;
}
$properties = $crmData['properties'];
$crmId = (string) $crmData['id'];
$accountId = $this->resolveContactAccount($properties, $accountMappings);
$data = $this->buildContactData($crmId, $properties, $accountId);
return $this->crmEntityRepository->importContact($this->config, $data);
}
private function resolveContactAccount(array $properties, array $accountMappings): ?int
{
if (empty($properties['associatedcompanyid'])) {
return null;
}
$companyId = (string) $properties['associatedcompanyid'];
if (! empty($accountMappings)) {
return $accountMappings[$companyId] ?? null;
}
return $this->crmEntityRepository->findAccountByExternalId(
$this->team->getCrmConfiguration(),
$companyId
)?->getId() ?? $this->syncAccount($companyId)?->getId();
}
private function buildContactData(string $crmId, array $properties, ?int $accountId): array
{
$countryCode = $this->buildContactCountry($properties);
$name = $this->buildContactName($properties);
$photoPath = $this->teamService->generateAvatar(
$crmId,
empty($name) ? ($properties['email'] ?? 'N/A') : $name,
);
$parsedNumber = $this->buildContactPhone($countryCode, $properties);
$mobileNumber = $this->buildContactMobilePhone($countryCode, $properties);
$ownerId = $properties['hubspot_owner_id'] ?? null;
$profile = $ownerId !== null
? $this->crmEntityRepository->findProfileByExternalId($this->config, (string) $ownerId)
: null;
$ext = (isset($parsedNumber['ext']) && is_string($parsedNumber['ext']) && strlen($parsedNumber['ext']) <= 10)
? $parsedNumber['ext']
: null;
$title = isset($properties['jobtitle']) ? mb_strimwidth($properties['jobtitle'], 0, 128) : null;
$email = isset($properties['email']) ? mb_strimwidth($properties['email'], 0, 191) : null;
$remotelyCreatedAt = ! empty($properties['createdate']) ? Carbon::parse($properties['createdate']) : null;
return [
'crm_provider_id' => $crmId,
'team_id' => $this->team->getId(),
'account_id' => $accountId,
'user_id' => $profile?->getUserId(),
'owner_id' => $ownerId,
'name' => $name,
'title' => $title,
'email' => $email,
'country_code' => $countryCode,
'phone' => $parsedNumber['phone'] ?? null,
'mobile_phone' => $mobileNumber ?? null,
'ext' => $ext,
'photo_path' => $photoPath,
'remotely_created_at' => $remotelyCreatedAt,
];
}
/**
* @param $properties
*/
private function buildContactName($properties): string
{
if (is_array($properties)) {
return $this->buildContactNameFromArray($properties);
}
return $this->buildContactNameFromObject($properties);
}
private function buildContactNameFromArray(array $properties): string
{
if (! empty($properties['name'])) {
return mb_strimwidth($properties['name'], 0, 100);
}
$name = '';
if (! empty($properties['firstname'])) {
$name = $properties['firstname'] . ' ';
}
if (! empty($properties['lastname'])) {
$name .= $properties['lastname'];
}
if ($name === '' && ! empty($properties['email'])) {
$name = $properties['email'];
}
return mb_strimwidth($name, 0, 100);
}
private function buildContactNameFromObject($properties): string
{
$name = '';
if (isset($properties->firstname)) {
$name = $properties->firstname->value . ' ';
}
if (isset($properties->lastname)) {
$name .= $properties->lastname->value;
}
if ($name === '' && isset($properties->email)) {
$name = $properties->email->value;
}
return mb_strimwidth($name, 0, 100);
}
/**
* @param $properties
*/
private function buildContactPhone(?string $countryCode, $properties): ?array
{
if (is_array($properties) && empty($properties['phone']) === false) {
$number = mb_strimwidth($properties['phone'], 0, 25);
return parsePhoneNumber($countryCode, $number);
} elseif (isset($properties->phone)) {
$number = mb_strimwidth($properties->phone->value, 0, 25);
return parsePhoneNumber($countryCode, $number);
}
return [];
}
/**
* @param $properties
*/
private function buildContactMobilePhone(?string $countryCode, $properties): ?string
{
return isset($properties['mobilephone'])
? Str::limit(phone_e164($countryCode, $properties['mobilephone']), 25, '')
: null;
}
/**
* @param $properties
* @param $account
*/
private function buildContactCountry($properties): ?string
{
if (is_array($properties) && empty($properties['country']) === false) {
return $this->convertCountryNameToCode($properties['country']);
}
if (isset($properties->country)) {
return $this->convertCountryNameToCode($properties->country->value);
}
return null;
}
/**
* HubSpot doesn't have leads, so this method does nothing.
*
* @param Carbon $since
* @param Carbon|null $to
* @param string|null $crmProfileId
*
* @return int
*/
public function syncLeads(Carbon $since, ?Carbon $to = null, ?string $crmProfileId = null): int
{
// Mark unused parameters to avoid code smell warnings
unset($since, $to, $crmProfileId);
return 0;
}
/**
* HubSpot doesn't have leads.
*
* @param string $crmId
*
* @inheritdoc
*/
public function syncLead(string $crmId): ?Lead
{
// Mark unused parameter to avoid code smell warnings
unset($crmId);
return null;
}
/**
* Sync accounts (companies) modified since a given date (manual sync mode).
*
* This method fetches companies from HubSpot API based on modification date and
* imports them one by one. It is used for:
* - Manual sync commands (e.g., crm:sync-account with --from parameter)
* - Initial sync for new teams
* - Backfill operations
*
* For regular sync webhook batchSyncCompanies is used:
*
* @param Carbon $since Fetch companies modified after this date
* @param Carbon|null $to Optional end date for modification range
*
* @return int Number of companies successfully synced
*/
public function syncAccounts(Carbon $since, ?Carbon $to = null): int
{
$syncCount = 0;
try {
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$fields = $this->getCompanyFields();
$payload = $this->payloadBuilder->getRecentlyUpdatedSearchPayload($since, $to, $fields);
foreach ($this->client->getPaginatedDataGenerator($payload, 'companies') as $hsAccount) {
$this->importAccount($hsAccount);
$syncCount++;
}
} catch (Exception $exception) {
$this->logger->error('[' . $this->getDisplayName() . '] Sync accounts failed', [
'teamId' => $this->team->getUuid(),
'reason' => $exception->getMessage(),
]);
}
return $syncCount;
}
/**
* @inheritdoc
*/
public function syncAccount(string $crmId): ?Account
{
try {
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$fields = $this->getCompanyFields();
$hsAccount = $this->client->getAccountById($crmId, $fields);
} catch (\HubSpot\Client\Crm\Companies\ApiException $e) {
$this->logger->info('[' . $this->getDisplayName() . '] Account fetch failed', [
'teamId' => $this->team->getUuid(),
'crmId' => $crmId,
'reason' => $e->getMessage(),
]);
return null;
} catch (CrmException $e) {
$this->logger->info('[' . $this->getDisplayName() . '] Account not found', [
'teamId' => $this->team->getUuid(),
'crmId' => $crmId,
'reason' => $e->getMessage(),
]);
return null;
}
return $this->importAccount($hsAccount);
}
/**
* Process webhook-collected contact batches.
*
* Drains Redis sets containing contact CRM IDs collected from webhook events
* and dispatches ImportContactBatch jobs for batch processing.
*
* @return int Number of contact IDs dispatched to jobs
*/
public function batchSyncContacts(): int
{
$configId = $this->team->getCrmConfiguration()->getId();
return $this->batchProcessor->processBatchesForObjectType(
WebhookSyncBatchProcessor::OBJECT_TYPE_CONTACT,
$configId
);
}
public function importContactBatchByIds(array $crmIds): array
{
$result = [
'success_count' => 0,
'failed_ids' => [],
'errors' => [],
];
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$batchStart = microtime(true);
$slowContacts = [];
$fetchStart = microtime(true);
$allContacts = $this->fetchContactsByIdsInChunks($crmIds);
$fetchMs = (int) round((microtime(true) - $fetchStart) * 1000);
$fetchedIds = array_map('strval', array_column($allContacts, 'id'));
$notFoundIds = array_values(array_diff(array_map('strval', $crmIds), $fetchedIds));
if (! empty($notFoundIds)) {
$this->logger->info('[' . $this->getDisplayName() . '] Contact CRM IDs not found in HubSpot', [
'teamId' => $this->team->getId(),
'notFoundCount' => \count($notFoundIds),
'notFoundIds' => $notFoundIds,
'requestedCount' => \count($crmIds),
'fetchedCount' => \count($allContacts),
]);
}
if (empty($allContacts)) {
return $result;
}
$prepareStart = microtime(true);
$accountMappings = $this->prepareAccountMappingsForContacts($allContacts);
$prepareAccountsMs = (int) round((microtime(true) - $prepareStart) * 1000);
$loopStart = microtime(true);
foreach ($allContacts as $contactData) {
$contactStart = microtime(true);
try {
$contact = $this->importContact($contactData, $accountMappings);
if ($contact !== null) {
$result['success_count']++;
}
} catch (\Throwable $e) {
$crmId = $contactData['id'] ?? 'unknown';
$result['failed_ids'][] = $crmId;
$result['errors'][$crmId] = $e->getMessage();
$this->logger->warning('[' . $this->getDisplayName() . '] Failed to import contact', [
'teamId' => $this->team->getId(),
'crmId' => $crmId,
'error' => $e->getMessage(),
]);
}
$contactMs = (int) round((microtime(true) - $contactStart) * 1000);
if ($contactMs > 1000) {
$slowContacts[] = ['crmId' => $contactData['id'] ?? 'unknown', 'ms' => $contactMs];
}
}
$loopMs = (int) round((microtime(true) - $loopStart) * 1000);
$totalMs = (int) round((microtime(true) - $batchStart) * 1000);
$this->logger->info('[' . $this->getDisplayName() . '] importContactBatch timing', [
'teamId' => $this->team->getId(),
'contact_count' => \count($allContacts),
'requested_count' => \count($crmIds),
'not_found_count' => \count($notFoundIds),
'total_ms' => $totalMs,
'fetch_api_ms' => $fetchMs,
'prepare_accounts_ms' => $prepareAccountsMs,
'contacts_loop_ms' => $loopMs,
'avg_contact_ms' => ! empty($allContacts) ? (int) round($loopMs / \count($allContacts)) : 0,
'slow_contacts_count' => \count($slowContacts),
'slow_contacts' => array_slice($slowContacts, 0, 10),
]);
return $result;
}
private function fetchContactsByIdsInChunks(array $crmIds): array
{
$fields = $this->getContactFields();
$allContacts = [];
foreach (array_chunk($crmIds, self::BATCH_SIZE) as $chunk) {
try {
$contacts = $this->client->getContactsByIds($chunk, $fields);
foreach ($contacts as $contactData) {
$allContacts[] = $contactData;
}
} catch (\Throwable $e) {
// @TODO what will happen if this exception is thrown
$this->logger->warning('[' . $this->getDisplayName() . '] Batch contact fetch failed', [
'chunk_size' => \count($chunk),
'error' => $e->getMessage(),
]);
throw $e;
}
}
return $allContacts;
}
private function prepareAccountMappingsForContacts(array $contacts): array
{
$companyIds = [];
foreach ($contacts as $contact) {
$companyId = $contact['properties']['associatedcompanyid'] ?? null;
if ($companyId !== null && $companyId !== '') {
$companyIds[] = (string) $companyId;
}
}
$companyIds = array_unique($companyIds);
if (empty($companyIds)) {
return [];
}
$mappings = $this->crmEntityRepository
->getExistingAccountIdsMap($this->config, $companyIds);
$missingCompanyIds = array_diff($companyIds, array_keys($mappings));
if (empty($missingCompanyIds)) {
return $mappings;
}
$this->logger->info('[' . $this->getDisplayName() . '] Batch syncing missing accounts for contacts', [
'teamId' => $this->team->getId(),
'total_companies' => \count($companyIds),
'existing_companies' => \count($mappings),
'missing_companies' => \count($missingCompanyIds),
]);
try {
$syncedAccounts = $this->batchSyncAccountsForContacts($missingCompanyIds);
$mappings = array_merge($mappings, $syncedAccounts);
} catch (\Throwable $e) {
$this->logger->error('[' . $this->getDisplayName() . '] Failed to batch sync missing accounts', [
'teamId' => $this->team->getId(),
'missingCompanyIds' => $missingCompanyIds,
'missingCount' => count($missingCompanyIds),
'error' => $e->getMessage(),
]);
}
return $mappings;
}
private function batchSyncAccountsForContacts(array $companyIds): array
{
$syncedAccounts = [];
$fields = $this->getCompanyFields();
foreach (array_chunk($companyIds, self::BATCH_SIZE) as $chunk) {
try {
$companies = $this->client->getCompaniesByIds($chunk, $fields);
foreach ($companies as $companyData) {
try {
$account = $this->importAccount($companyData);
if ($account) {
$syncedAccounts[$account->getCrmProviderId()] = $account->getId();
}
} catch (\Throwable $e) {
$this->logger->warning('[' . $this->getDisplayName() . '] Failed to import account in batch', [
'teamId' => $this->team->getId(),
'companyId' => $companyData['id'] ?? 'unknown',
'error' => $e->getMessage(),
]);
}
}
} catch (\Throwable $e) {
$this->logger->warning('[' . $this->getDisplayName() . '] Batch company fetch failed', [
'teamId' => $this->team->getId(),
'chunk_size' => \count($chunk),
'error' => $e->getMessage(),
]);
}
}
return $syncedAccounts;
}
/**
* Process webhook-collected company batches.
*
* Drains Redis sets containing company CRM IDs collected from webhook events
* and dispatches ImportAccountBatch jobs for batch processing.
*
* @return int Number of company IDs dispatched to jobs
*/
public function batchSyncCompanies(): int
{
$configId = $this->team->getCrmConfiguration()->getId();
return $this->batchProcessor->processBatchesForObjectType(
WebhookSyncBatchProcessor::OBJECT_TYPE_COMPANY,
$configId
);
}
public function importAccountBatchByIds(array $crmIds): array
{
$result = [
'success_count' => 0,
'failed_ids' => [],
'errors' => [],
];
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$batchStart = microtime(true);
$slowAccounts = [];
$fields = $this->getCompanyFields();
$allCompanies = [];
$fetchStart = microtime(true);
foreach (array_chunk($crmIds, self::BATCH_SIZE) as $chunk) {
try {
$companies = $this->client->getCompaniesByIds($chunk, $fields);
foreach ($companies as $companyData) {
$allCompanies[] = $companyData;
}
} catch (\Throwable $e) {
$this->logger->warning('[' . $this->getDisplayName() . '] Batch company fetch failed', [
'chunk_size' => \count($chunk),
'error' => $e->getMessage(),
]);
throw $e;
}
}
$fetchMs = (int) round((microtime(true) - $fetchStart) * 1000);
$fetchedIds = array_map('strval', array_column($allCompanies, 'id'));
$notFoundIds = array_values(array_diff(array_map('strval', $crmIds), $fetchedIds));
if (! empty($notFoundIds)) {
$this->logger->info('[' . $this->getDisplayName() . '] Company CRM IDs not found in HubSpot', [
'teamId' => $this->team->getId(),
'notFoundCount' => \count($notFoundIds),
'notFoundIds' => $notFoundIds,
'requestedCount' => \count($crmIds),
'fetchedCount' => \count($allCompanies),
]);
}
$loopStart = microtime(true);
foreach ($allCompanies as $companyData) {
$accountStart = microtime(true);
try {
$account = $this->importAccount($companyData);
if ($account !== null) {
$result['success_count']++;
}
} catch (\Throwable $e) {
$crmId = $companyData['id'] ?? $companyData['properties']['hs_object_id'] ?? 'unknown';
$result['failed_ids'][] = $crmId;
$result['errors'][$crmId] = $e->getMessage();
$this->logger->warning('[' . $this->getDisplayName() . '] Failed to import account', [
'crmId' => $crmId,
'error' => $e->getMessage(),
]);
}
$accountMs = (int) round((microtime(true) - $accountStart) * 1000);
if ($accountMs > 1000) {
$crmId = $companyData['id'] ?? $companyData['properties']['hs_object_id'] ?? 'unknown';
$slowAccounts[] = ['crmId' => $crmId, 'ms' => $accountMs];
}
}
$loopMs = (int) round((microtime(true) - $loopStart) * 1000);
$totalMs = (int) round((microtime(true) - $batchStart) * 1000);
$this->logger->info('[' . $this->getDisplayName() . '] importAccountBatch timing', [
'teamId' => $this->team->getId(),
'account_count' => \count($allCompanies),
'requested_count' => \count($crmIds),
'not_found_count' => \count($notFoundIds),
'total_ms' => $totalMs,
'fetch_api_ms' => $fetchMs,
'accounts_loop_ms' => $loopMs,
'avg_account_ms' => ! empty($allCompanies) ? (int) round($loopMs / \count($allCompanies)) : 0,
'slow_accounts_count' => \count($slowAccounts),
'slow_accounts' => array_slice($slowAccounts, 0, 10),
]);
return $result;
}
private function getCompanyFields(): array
{
return [
'country',
'name',
'phone',
'domain',
'hs_avatar_filemanager_key',
'industry',
'hubspot_owner_id',
'hs_object_id',
'createdate',
'hs_lastmodifieddate',
];
}
private function importAccount($crmData): ?Account
{
$crmProviderId = $crmData['id'] ?? $crmData['properties']['hs_object_id'] ?? null;
$this->logger->info('[HubSpot] importAccount', [
'crm_provider_id' => $crmProviderId,
'config_id' => $this->config->getId(),
]);
if (empty($crmData['properties'])) {
$this->logger->info('[HubSpot] importAccount failed: empty properties', [
'crm_provider_id' => $crmProviderId,
'config_id' => $this->config->getId(),
]);
return null;
}
$properties = $crmData['properties'];
$crmId = (string) $properties['hs_object_id'];
$countryCode = isset($properties['country']) ? $this->convertCountryNameToCode($properties['country']) : null;
if (isset($properties['phone'])) {
// Trim to our width and attempt to parse it.
$number = mb_strimwidth($properties['phone'], 0, 25);
$parsedNumber = parsePhoneNumber($countryCode, $number);
} else {
$parsedNumber = [];
}
$name = '[unknown]';
if (isset($properties['name'])) {
$name = $properties['name'];
}
$photoPath = $this->prospectPhotoPathService->getOrGeneratePhotoPath(
$this->config,
$crmId,
Account::class,
$crmId,
$name
);
$industry = null;
if (isset($properties['industry'])) {
$industry = mb_strimwidth($properties['industry'], 0, 40);
}
$ownerId = $profile = null;
if (isset($properties['hubspot_owner_id'])) {
$ownerId = $properties['hubspot_owner_id'];
$profile = $this->crmEntityRepository->findProfileByExternalId($this->config, (string) $ownerId);
}
$domain = null;
if (isset($properties['domain'])) {
$domain = StringUtil::resolveDomain($properties['domain']);
}
$remotelyCreatedAt = null;
if (isset($properties['createdate']) && ! empty($properties['createdate'])) {
$remotelyCreatedAt = Carbon::parse($properties['createdate']);
}
$data = [
'crm_provider_id' => $crmId,
'team_id' => $this->team->id,
'user_id' => $profile ? $profile->user_id : null,
'owner_id' => $ownerId,
'name' => mb_strimwidth($name, 0, 191),
'photo_path' => $photoPath,
'industry' => $industry,
'domain' => $domain !== null
? substr($domain, 0, 191)
: null,
'phone' => $parsedNumber['phone'] ?? null,
'ext' => $parsedNumber['ext'] ?? null,
'country_code' => $countryCode,
'remotely_created_at' => $remotelyCreatedAt,
];
return $this->crmEntityRepository->importAccount($this->config, $data);
}
public function deleteContact(string $crmProviderId): bool
{
try {
$contact = $this->crmEntityRepository->findContactByExternalId($this->config, $crmProviderId);
if (! $contact) {
$this->logger->info('[HubSpot] Contact not found for deletion', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
// return success because we do not know which instance is the target
return true;
}
$id = $contact->getId();
$this->logger->info('[HubSpot] Deleting contact via webhook', [
'contact_id' => $id,
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
$contact->delete();
DeleteContactJob::dispatch($id)->afterCommit();
return true;
} catch (Exception $e) {
$this->logger->error('[HubSpot] Failed to delete contact via webhook', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
'error' => $e->getMessage(),
]);
return false;
}
}
public function deleteAccount(string $crmProviderId): bool
{
try {
$account = $this->crmEntityRepository->findAccountByExternalId($this->config, $crmProviderId);
if (! $account) {
$this->logger->info('[HubSpot] Account not found for deletion', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
// return success because we do not know which instance is the target
return true;
}
$id = $account->getId();
$this->logger->info('[HubSpot] Deleting account via webhook', [
'account_id' => $id,
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
$account->delete();
DeleteAccountJob::dispatch($id)->afterCommit();
return true;
} catch (Exception $e) {
$this->logger->error('[HubSpot] Failed to delete account via webhook', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
'error' => $e->getMessage(),
]);
return false;
}
}
public function deleteOpportunity(string $crmProviderId): bool
{
try {
$opportunity = $this->crmEntityRepository->findOpportunityByExternalId($this->config, $crmProviderId);
if (! $opportunity) {
$this->logger->info('[HubSpot] Opportunity not found for deletion', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
// return success because we do not know which instance is the target
return true;
}
$id = $opportunity->getId();
$this->logger->info('[HubSpot] Deleting opportunity via webhook', [
'opportunity_id' => $id,
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
$opportunity->delete();
DeleteOpportunityJob::dispatch($id)->afterCommit();
return true;
} catch (Exception $e) {
$this->logger->error('[HubSpot] Failed to delete opportunity via webhook', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
'error' => $e->getMessage(),
]);
return false;
}
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide
app ~/jiminny/app, folder
.circleci, folder
.cursor, folder
.github
.sonarlint, folder
.vscode, folder
.windsurf, folder
app, sources root
Actions, folder
Component, folder
Acl, folder
ActionItems, folder
Activity, folder
ActivityAnalytics, folder
ActivitySearch, folder
AiActivityType, folder
AiAutomation, folder
AiCallScoring, folder
AskAnything, folder
Dtos, folder
Events, folder
AskAnythingPromptService.php
HistoryService.php
AskJiminnyAi, folder
AWS, folder
BillingManagement, folder
Cache, folder
CoachingFeedback, folder
Country, folder
CustomerApi, folder
Database, folder
Datadog, folder
DateTime, folder
DealInsights, folder
DealRisks, folder
ElasticSearch
Eloquent
Encoding
Encryption
ES
Faker
FeatureFlags
FFMpeg
FileSystem
Gecko
Gong
GuzzleHttp
KeyPoints
Kiosk
LanguageDetection
LiveFeed
Locks
Math
MediaPipeline
MeetingBot, folder
MobileSettings, folder
Model, folder
Notification, folder
Nudge, folder
ParagraphBreaker, folder
ParticipantSpeech, folder
PartitionedCookie, folder
PlaybackPage, folder
Playlist, folder
Prophet, folder
ProphetAi, folder
ProsperWorks, folder
Queue, folder
Job, folder
RateLimitAware.php, abstract class
RateLimitAwareWrapper.php, class
BotsQueueConstants.php
Constants.php
ProcessingQueueConstants.php
Router, folder
Saml2, folder
SCIM, folder
Seeder, folder
Sentry, folder
Serializer, folder
Settings, folder
Sidekick, folder
Slack, folder
TeamInsights, folder
TimeMemoryMapper, folder
Transcription, folder
TranscriptionSummary, folder
Twilio, folder
Uploader, folder
UrlGenerator, folder
Utility, folder
Exceptions, folder
Service, folder
BaseRateLimiter.php, class
EfficientJsonParser.php, class
ProviderRateLimiter.php, class
RateLimiterInstance.php, class
Uuid, folder
Waveform, folder
Webhooks, folder
Workflow, folder
Configuration, folder
Console, folder
Commands, folder
Activities, folder
Analytics, folder
Calendars, folder
Crm, folder
Hubspot, folder
IntegrationApp, folder
Traits, folder
AddLayoutEntities.php, class
AutologDelayedCommand.php, class
BullhornCommandAbstract.php, abstract class
BullhornPingCommand.php, class
BullhornSearchCommand.php, class
BullhornSessionCommand.php, class
CheckActivityLoggableCommand.php, final class
CleanDuplicateFieldDataCommand.php, class
FullSyncOpportunityCommand.php, class
LogActivitiesCommand.php, final class
ManageSyncStrategyCommand.php, class
MatchCrmObjectsCommand.php, class
MatchOpportunityActivitiesCommand.php, class
MigrateProvider.php, class
ProcessHubspotObjectsSyncBatches.php, class
PurgeDeletedOpportunitiesCommand.php, class
ResetGovernorLimits.php, class
SendNotLogged.php, class
SetupActivityTypeForFollowUp.php, final class
SetupCloseCrm.php, class
SetupCopperCrm.php, class
SetupCrmCommand.php, abstract class
SetupLayouts.php, class
SyncAccount.php, class
SyncContact.php, class
SyncFieldMetadata.php, class
SyncHubspotActiveDeals.php, class
SyncHubspotObjects.php, class
SyncLead.php, class
SyncObjects.php, class
SyncOpportunitiesMissingFieldDataCommand.php, class
SyncOpportunity.php, class
SyncProfileMetadata.php, class
SyncTeamMetadata.php, class
UpdateOpportunitySpecifications.php, class
DealInsights, folder
Dev, folder
AddRateLimitCommand.php, class
FixHubSpotTokens.php, class
FixMissMatchedCrmActivitiesCommand.php, final class
ImportCallsCommand.php, class
MonitorSocialAccountsState.php, class
Dialers, folder
DTOs, folder
Elasticsearch, folder
EngagementStats, folder
GeckoExport, folder
Livestream, folder
Mailboxes, folder
Migrate, folder
PlaybackThemes, folder
Playbooks, folder
Playlists, folder
Postmark, folder
ProphetAi, folder
Reports, folder
AutomatedReportsCommand.php, class
AutomatedReportsRetentionPolicyCommand.php, class
AutomatedReportsSendCommand.php, class
CreateMockAskJiminnyReportResultCommand.php, class
DeleteReportCommand.php, class
GenerateMarketingReport.php, class
Team.php, class
Usage.php, class
Slack, folder
Teams, folder
Tracks, folder
Transcription, folder
Twilio, folder
Users, folder
Vocabulary, folder
Zoom, folder
Command.php
CreateDatabaseUsers.php
DatabaseTableCount.php
DeleteOldAiCrmNotesCommand.php
DeleteS3LeftoversCommand.php
DevPostmanCommand.php
DiarizeViaAiParticipantIdentificationCommand.php
EncryptTokensCommand.php
EngagementStatsRegenerateCommand.php
FeatureFlagsHelper.php
FixCrossTenantIssues.php
FlushRolesPermissionsCache.php
GenerateInternalWebhookToken.php
GroupSetDefaultLanguageCommand.php
HelperTruncateCoachingTables.php
HubspotJournalPollingCommand.php
HubspotWebhookServiceCommand.php
ImportRecording.php
ImportUsersFromCsvFile.php
IterateUsersCommand.php
JiminnyCacheClearCommand.php
JiminnyDebugCommand.php
JiminnySetEncryptedTokenManagerModeCommand.php
JiminnyTokenInfoCommand.php
MakeSlackLiveCoachingChatNotesOn.php
ManageScimForTeam.php
MarkBranchForEnvironmentPipelineCommand.php
MuteOrganizerChannel.php, class
PhpApm.php, class
PropagateCoachingFeedbackCreatedAtToSectionFeedbacks.php, class...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
2840
|
113
|
37
|
2026-05-07T11:43:01.772616+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778154181772_m1.jpg...
|
PhpStorm
|
faVsco.js – Service.php
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
iTerm2ShellEditViewSessionScriptsProfilesWindowHel iTerm2ShellEditViewSessionScriptsProfilesWindowHelp$0Support Daily - in 17 m100% <478DEV (docker)DOCKER0 81DEV (docker)882APP (-zsh)-zshjiminny-worker-processing-4:j1minny-worker-processing-4_00:jiminny-worker-processing-5:jiminny-worker-processing-5_00:stoppedstoppedworker-analytics:worker-analytics_00: stoppedworker-crm-update:worker-crm-update_00: stoppedartisan-schedule:artisan-schedule_00:stoppedworker-nudges:worker-nudges_00: stoppedworker:worker_00: stoppedjiminny-worker-processing-1:jiminny-worker-processing-1_00: stoppedworker-audio:worker-audio_00: stoppedworker-calendar:worker-calendar_00:stoppedworker-conferences:worker-conferences_00:stoppedworker-crm-sync:worker-crm-sync_00:stoppedworker-emails:worker-emails_00:stoppedworker-es-update:worker-es-update_00: stoppedartisan-schedule:artisan-schedule_00: startedjiminny-worker-processing-1:jiminny-worker-processing-1_00: startedjiminny-worker-processing-2:jiminny-worker-processing-2_00: startedjiminny-worker-processing-3:jiminny-worker-processing-3_00: startedjiminny-worker-processing-4:jiminny-worker-processing-4_00: startedjiminny-worker-processing-5:jiminny-worker-processing-5_00: startedjiminny-worker-processing-delayed: jiminny-worker-processing-delayed_00: startedworker:worker_00: startedworker-analytics:worker-analytics_00: startedworker-audio:worker-audio_00: startedworker-calendar:worker-calendar_00: startedworker-conferences:worker-conferences_00: startedworker-crm-sync:worker-crm-sync_00: startedworker-crm-update:worker-crm-update_00: startedworker-download:worker-download_00:startedworker-emails:worker-emails_00: startedworker-es-update:worker-es-update_00: startedworker-nudges:worker-nudges_00: startedroot@docker_lamp_1:/home/jiminny# php artisan crm:sync-opportunity --teamId=2 --from='2026-05-01 00:00:00'Syncing opportunity for HubspotSyncing opportunities modified since 2026-05-01 00:00:00...Synced 6 opportunities.root@docker_lamp_1:/home/jiminny# php artisan crm:sync-opportunity --teamId=2 --opportunityId 374720564Syncing opportunity for HubspotSyncing opportunity 374720564...Synced AmirHSOpp to 5066root@docker_lamp_1:/home/jiminny# php artisan crm: sync-contact --teamId=2 --contactId 21351Syncing contact(s) for HubspotSyncing contact 21351...Synced Lissy Newlandto 464root@docker_lamp_1:/home/jiminny# ]• $4screenpipe*•$5-zshThu 7 May 14:43:01T81₴6DEV...
|
NULL
|
-7838019374616982401
|
NULL
|
click
|
ocr
|
NULL
|
iTerm2ShellEditViewSessionScriptsProfilesWindowHel iTerm2ShellEditViewSessionScriptsProfilesWindowHelp$0Support Daily - in 17 m100% <478DEV (docker)DOCKER0 81DEV (docker)882APP (-zsh)-zshjiminny-worker-processing-4:j1minny-worker-processing-4_00:jiminny-worker-processing-5:jiminny-worker-processing-5_00:stoppedstoppedworker-analytics:worker-analytics_00: stoppedworker-crm-update:worker-crm-update_00: stoppedartisan-schedule:artisan-schedule_00:stoppedworker-nudges:worker-nudges_00: stoppedworker:worker_00: stoppedjiminny-worker-processing-1:jiminny-worker-processing-1_00: stoppedworker-audio:worker-audio_00: stoppedworker-calendar:worker-calendar_00:stoppedworker-conferences:worker-conferences_00:stoppedworker-crm-sync:worker-crm-sync_00:stoppedworker-emails:worker-emails_00:stoppedworker-es-update:worker-es-update_00: stoppedartisan-schedule:artisan-schedule_00: startedjiminny-worker-processing-1:jiminny-worker-processing-1_00: startedjiminny-worker-processing-2:jiminny-worker-processing-2_00: startedjiminny-worker-processing-3:jiminny-worker-processing-3_00: startedjiminny-worker-processing-4:jiminny-worker-processing-4_00: startedjiminny-worker-processing-5:jiminny-worker-processing-5_00: startedjiminny-worker-processing-delayed: jiminny-worker-processing-delayed_00: startedworker:worker_00: startedworker-analytics:worker-analytics_00: startedworker-audio:worker-audio_00: startedworker-calendar:worker-calendar_00: startedworker-conferences:worker-conferences_00: startedworker-crm-sync:worker-crm-sync_00: startedworker-crm-update:worker-crm-update_00: startedworker-download:worker-download_00:startedworker-emails:worker-emails_00: startedworker-es-update:worker-es-update_00: startedworker-nudges:worker-nudges_00: startedroot@docker_lamp_1:/home/jiminny# php artisan crm:sync-opportunity --teamId=2 --from='2026-05-01 00:00:00'Syncing opportunity for HubspotSyncing opportunities modified since 2026-05-01 00:00:00...Synced 6 opportunities.root@docker_lamp_1:/home/jiminny# php artisan crm:sync-opportunity --teamId=2 --opportunityId 374720564Syncing opportunity for HubspotSyncing opportunity 374720564...Synced AmirHSOpp to 5066root@docker_lamp_1:/home/jiminny# php artisan crm: sync-contact --teamId=2 --contactId 21351Syncing contact(s) for HubspotSyncing contact 21351...Synced Lissy Newlandto 464root@docker_lamp_1:/home/jiminny# ]• $4screenpipe*•$5-zshThu 7 May 14:43:01T81₴6DEV...
|
2838
|
NULL
|
NULL
|
NULL
|
|
2839
|
114
|
34
|
2026-05-07T11:43:01.971562+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778154181971_m2.jpg...
|
PhpStorm
|
faVsco.js – SyncCrmEntitiesTrait.php
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
Editor for custom.log
Sync Changes
Hide This Notification
Code changed:
Hide
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\ServiceTraits;
use Carbon\Carbon;
use Exception;
use Illuminate\Support\Str;
use Jiminny\Exceptions\CrmException;
use Jiminny\Jobs\Crm\Delete\DeleteAccountJob;
use Jiminny\Jobs\Crm\Delete\DeleteContactJob;
use Jiminny\Jobs\Crm\Delete\DeleteOpportunityJob;
use Jiminny\Models\Account;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Repositories\Crm\CrmEntityRepository;
use Jiminny\Services\Avatar\ProspectPhotoPathService;
use Jiminny\Services\Crm\Hubspot\HubspotClientInterface;
use Jiminny\Services\Crm\Hubspot\WebhookSyncBatchProcessor;
use Jiminny\Utils\StringUtil;
trait SyncCrmEntitiesTrait
{
use OpportunitySyncTrait;
private const string CDN_URL = '[URL_WITH_CREDENTIALS] Carbon $since Fetch contacts modified after this date
* @param Carbon|null $to Optional end date for modification range
*
* @return int Number of contacts successfully synced
*/
public function syncContacts(Carbon $since, ?Carbon $to = null): int
{
$syncCount = 0;
try {
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$fields = $this->getContactFields();
$payload = $this->payloadBuilder->getRecentlyUpdatedSearchPayload($since, $to, $fields);
foreach ($this->client->getPaginatedDataGenerator($payload, 'contacts') as $hsContact) {
$this->importContact($hsContact);
$syncCount++;
}
} catch (Exception $exception) {
$this->logger->error('[' . $this->getDisplayName() . '] Sync contacts failed', [
'teamId' => $this->team->getUuid(),
'reason' => $exception->getMessage(),
]);
}
return $syncCount;
}
/**
* @inheritdoc
*/
public function syncContact(string $crmId): ?Contact
{
try {
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$fields = $this->getContactFields();
$hsContact = $this->client->getContactById($crmId, $fields);
} catch (\HubSpot\Client\Crm\Contacts\ApiException $e) {
$this->logger->info('[' . $this->getDisplayName() . '] Contact fetch failed', [
'teamId' => $this->team->getUuid(),
'crmId' => $crmId,
'reason' => $e->getMessage(),
]);
return null;
} catch (CrmException $e) {
$this->logger->info('[' . $this->getDisplayName() . '] Contact not found', [
'teamId' => $this->team->getUuid(),
'crmId' => $crmId,
'reason' => $e->getMessage(),
]);
return null;
}
if (empty($hsContact['properties']) || empty($hsContact['id'])) {
$this->logger->warning('[' . $this->getDisplayName() . '] Contact data incomplete', [
'teamId' => $this->team->getUuid(),
'crmId' => $crmId,
'has_properties' => ! empty($hsContact['properties']),
'has_id' => ! empty($hsContact['id']),
]);
return null;
}
return $this->importContact($hsContact);
}
private function getContactFields(): array
{
return [
'associatedcompanyid',
'country',
'firstname',
'lastname',
'phone',
'mobilephone',
'email',
'photo',
'hs_avatar_filemanager_key',
'jobtitle',
'hubspot_owner_id',
'hs_object_id',
'createdate',
'hs_lastmodifieddate',
];
}
/**
* @inheritdoc
*/
private function importContact($crmData, array $accountMappings = []): ?Contact
{
$crmProviderId = $crmData['id'] ?? null;
$this->logger->info('[HubSpot] importContact', [
'crm_provider_id' => $crmProviderId,
'config_id' => $this->config->getId(),
]);
if (empty($crmData['properties'])) {
$this->logger->info('[HubSpot] importContact failed: empty properties', [
'crm_provider_id' => $crmProviderId,
'config_id' => $this->config->getId(),
]);
return null;
}
$properties = $crmData['properties'];
$crmId = (string) $crmData['id'];
$accountId = $this->resolveContactAccount($properties, $accountMappings);
$data = $this->buildContactData($crmId, $properties, $accountId);
return $this->crmEntityRepository->importContact($this->config, $data);
}
private function resolveContactAccount(array $properties, array $accountMappings): ?int
{
if (empty($properties['associatedcompanyid'])) {
return null;
}
$companyId = (string) $properties['associatedcompanyid'];
if (! empty($accountMappings)) {
return $accountMappings[$companyId] ?? null;
}
return $this->crmEntityRepository->findAccountByExternalId(
$this->team->getCrmConfiguration(),
$companyId
)?->getId() ?? $this->syncAccount($companyId)?->getId();
}
private function buildContactData(string $crmId, array $properties, ?int $accountId): array
{
$countryCode = $this->buildContactCountry($properties);
$name = $this->buildContactName($properties);
$photoPath = $this->teamService->generateAvatar(
$crmId,
empty($name) ? ($properties['email'] ?? 'N/A') : $name,
);
$parsedNumber = $this->buildContactPhone($countryCode, $properties);
$mobileNumber = $this->buildContactMobilePhone($countryCode, $properties);
$ownerId = $properties['hubspot_owner_id'] ?? null;
$profile = $ownerId !== null
? $this->crmEntityRepository->findProfileByExternalId($this->config, (string) $ownerId)
: null;
$ext = (isset($parsedNumber['ext']) && is_string($parsedNumber['ext']) && strlen($parsedNumber['ext']) <= 10)
? $parsedNumber['ext']
: null;
$title = isset($properties['jobtitle']) ? mb_strimwidth($properties['jobtitle'], 0, 128) : null;
$email = isset($properties['email']) ? mb_strimwidth($properties['email'], 0, 191) : null;
$remotelyCreatedAt = ! empty($properties['createdate']) ? Carbon::parse($properties['createdate']) : null;
return [
'crm_provider_id' => $crmId,
'team_id' => $this->team->getId(),
'account_id' => $accountId,
'user_id' => $profile?->getUserId(),
'owner_id' => $ownerId,
'name' => $name,
'title' => $title,
'email' => $email,
'country_code' => $countryCode,
'phone' => $parsedNumber['phone'] ?? null,
'mobile_phone' => $mobileNumber ?? null,
'ext' => $ext,
'photo_path' => $photoPath,
'remotely_created_at' => $remotelyCreatedAt,
];
}
/**
* @param $properties
*/
private function buildContactName($properties): string
{
if (is_array($properties)) {
return $this->buildContactNameFromArray($properties);
}
return $this->buildContactNameFromObject($properties);
}
private function buildContactNameFromArray(array $properties): string
{
if (! empty($properties['name'])) {
return mb_strimwidth($properties['name'], 0, 100);
}
$name = '';
if (! empty($properties['firstname'])) {
$name = $properties['firstname'] . ' ';
}
if (! empty($properties['lastname'])) {
$name .= $properties['lastname'];
}
if ($name === '' && ! empty($properties['email'])) {
$name = $properties['email'];
}
return mb_strimwidth($name, 0, 100);
}
private function buildContactNameFromObject($properties): string
{
$name = '';
if (isset($properties->firstname)) {
$name = $properties->firstname->value . ' ';
}
if (isset($properties->lastname)) {
$name .= $properties->lastname->value;
}
if ($name === '' && isset($properties->email)) {
$name = $properties->email->value;
}
return mb_strimwidth($name, 0, 100);
}
/**
* @param $properties
*/
private function buildContactPhone(?string $countryCode, $properties): ?array
{
if (is_array($properties) && empty($properties['phone']) === false) {
$number = mb_strimwidth($properties['phone'], 0, 25);
return parsePhoneNumber($countryCode, $number);
} elseif (isset($properties->phone)) {
$number = mb_strimwidth($properties->phone->value, 0, 25);
return parsePhoneNumber($countryCode, $number);
}
return [];
}
/**
* @param $properties
*/
private function buildContactMobilePhone(?string $countryCode, $properties): ?string
{
return isset($properties['mobilephone'])
? Str::limit(phone_e164($countryCode, $properties['mobilephone']), 25, '')
: null;
}
/**
* @param $properties
* @param $account
*/
private function buildContactCountry($properties): ?string
{
if (is_array($properties) && empty($properties['country']) === false) {
return $this->convertCountryNameToCode($properties['country']);
}
if (isset($properties->country)) {
return $this->convertCountryNameToCode($properties->country->value);
}
return null;
}
/**
* HubSpot doesn't have leads, so this method does nothing.
*
* @param Carbon $since
* @param Carbon|null $to
* @param string|null $crmProfileId
*
* @return int
*/
public function syncLeads(Carbon $since, ?Carbon $to = null, ?string $crmProfileId = null): int
{
// Mark unused parameters to avoid code smell warnings
unset($since, $to, $crmProfileId);
return 0;
}
/**
* HubSpot doesn't have leads.
*
* @param string $crmId
*
* @inheritdoc
*/
public function syncLead(string $crmId): ?Lead
{
// Mark unused parameter to avoid code smell warnings
unset($crmId);
return null;
}
/**
* Sync accounts (companies) modified since a given date (manual sync mode).
*
* This method fetches companies from HubSpot API based on modification date and
* imports them one by one. It is used for:
* - Manual sync commands (e.g., crm:sync-account with --from parameter)
* - Initial sync for new teams
* - Backfill operations
*
* For regular sync webhook batchSyncCompanies is used:
*
* @param Carbon $since Fetch companies modified after this date
* @param Carbon|null $to Optional end date for modification range
*
* @return int Number of companies successfully synced
*/
public function syncAccounts(Carbon $since, ?Carbon $to = null): int
{
$syncCount = 0;
try {
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$fields = $this->getCompanyFields();
$payload = $this->payloadBuilder->getRecentlyUpdatedSearchPayload($since, $to, $fields);
foreach ($this->client->getPaginatedDataGenerator($payload, 'companies') as $hsAccount) {
$this->importAccount($hsAccount);
$syncCount++;
}
} catch (Exception $exception) {
$this->logger->error('[' . $this->getDisplayName() . '] Sync accounts failed', [
'teamId' => $this->team->getUuid(),
'reason' => $exception->getMessage(),
]);
}
return $syncCount;
}
/**
* @inheritdoc
*/
public function syncAccount(string $crmId): ?Account
{
try {
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$fields = $this->getCompanyFields();
$hsAccount = $this->client->getAccountById($crmId, $fields);
} catch (\HubSpot\Client\Crm\Companies\ApiException $e) {
$this->logger->info('[' . $this->getDisplayName() . '] Account fetch failed', [
'teamId' => $this->team->getUuid(),
'crmId' => $crmId,
'reason' => $e->getMessage(),
]);
return null;
} catch (CrmException $e) {
$this->logger->info('[' . $this->getDisplayName() . '] Account not found', [
'teamId' => $this->team->getUuid(),
'crmId' => $crmId,
'reason' => $e->getMessage(),
]);
return null;
}
return $this->importAccount($hsAccount);
}
/**
* Process webhook-collected contact batches.
*
* Drains Redis sets containing contact CRM IDs collected from webhook events
* and dispatches ImportContactBatch jobs for batch processing.
*
* @return int Number of contact IDs dispatched to jobs
*/
public function batchSyncContacts(): int
{
$configId = $this->team->getCrmConfiguration()->getId();
return $this->batchProcessor->processBatchesForObjectType(
WebhookSyncBatchProcessor::OBJECT_TYPE_CONTACT,
$configId
);
}
public function importContactBatchByIds(array $crmIds): array
{
$result = [
'success_count' => 0,
'failed_ids' => [],
'errors' => [],
];
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$batchStart = microtime(true);
$slowContacts = [];
$fetchStart = microtime(true);
$allContacts = $this->fetchContactsByIdsInChunks($crmIds);
$fetchMs = (int) round((microtime(true) - $fetchStart) * 1000);
$fetchedIds = array_map('strval', array_column($allContacts, 'id'));
$notFoundIds = array_values(array_diff(array_map('strval', $crmIds), $fetchedIds));
if (! empty($notFoundIds)) {
$this->logger->info('[' . $this->getDisplayName() . '] Contact CRM IDs not found in HubSpot', [
'teamId' => $this->team->getId(),
'notFoundCount' => \count($notFoundIds),
'notFoundIds' => $notFoundIds,
'requestedCount' => \count($crmIds),
'fetchedCount' => \count($allContacts),
]);
}
if (empty($allContacts)) {
return $result;
}
$prepareStart = microtime(true);
$accountMappings = $this->prepareAccountMappingsForContacts($allContacts);
$prepareAccountsMs = (int) round((microtime(true) - $prepareStart) * 1000);
$loopStart = microtime(true);
foreach ($allContacts as $contactData) {
$contactStart = microtime(true);
try {
$contact = $this->importContact($contactData, $accountMappings);
if ($contact !== null) {
$result['success_count']++;
}
} catch (\Throwable $e) {
$crmId = $contactData['id'] ?? 'unknown';
$result['failed_ids'][] = $crmId;
$result['errors'][$crmId] = $e->getMessage();
$this->logger->warning('[' . $this->getDisplayName() . '] Failed to import contact', [
'teamId' => $this->team->getId(),
'crmId' => $crmId,
'error' => $e->getMessage(),
]);
}
$contactMs = (int) round((microtime(true) - $contactStart) * 1000);
if ($contactMs > 1000) {
$slowContacts[] = ['crmId' => $contactData['id'] ?? 'unknown', 'ms' => $contactMs];
}
}
$loopMs = (int) round((microtime(true) - $loopStart) * 1000);
$totalMs = (int) round((microtime(true) - $batchStart) * 1000);
$this->logger->info('[' . $this->getDisplayName() . '] importContactBatch timing', [
'teamId' => $this->team->getId(),
'contact_count' => \count($allContacts),
'requested_count' => \count($crmIds),
'not_found_count' => \count($notFoundIds),
'total_ms' => $totalMs,
'fetch_api_ms' => $fetchMs,
'prepare_accounts_ms' => $prepareAccountsMs,
'contacts_loop_ms' => $loopMs,
'avg_contact_ms' => ! empty($allContacts) ? (int) round($loopMs / \count($allContacts)) : 0,
'slow_contacts_count' => \count($slowContacts),
'slow_contacts' => array_slice($slowContacts, 0, 10),
]);
return $result;
}
private function fetchContactsByIdsInChunks(array $crmIds): array
{
$fields = $this->getContactFields();
$allContacts = [];
foreach (array_chunk($crmIds, self::BATCH_SIZE) as $chunk) {
try {
$contacts = $this->client->getContactsByIds($chunk, $fields);
foreach ($contacts as $contactData) {
$allContacts[] = $contactData;
}
} catch (\Throwable $e) {
// @TODO what will happen if this exception is thrown
$this->logger->warning('[' . $this->getDisplayName() . '] Batch contact fetch failed', [
'chunk_size' => \count($chunk),
'error' => $e->getMessage(),
]);
throw $e;
}
}
return $allContacts;
}
private function prepareAccountMappingsForContacts(array $contacts): array
{
$companyIds = [];
foreach ($contacts as $contact) {
$companyId = $contact['properties']['associatedcompanyid'] ?? null;
if ($companyId !== null && $companyId !== '') {
$companyIds[] = (string) $companyId;
}
}
$companyIds = array_unique($companyIds);
if (empty($companyIds)) {
return [];
}
$mappings = $this->crmEntityRepository
->getExistingAccountIdsMap($this->config, $companyIds);
$missingCompanyIds = array_diff($companyIds, array_keys($mappings));
if (empty($missingCompanyIds)) {
return $mappings;
}
$this->logger->info('[' . $this->getDisplayName() . '] Batch syncing missing accounts for contacts', [
'teamId' => $this->team->getId(),
'total_companies' => \count($companyIds),
'existing_companies' => \count($mappings),
'missing_companies' => \count($missingCompanyIds),
]);
try {
$syncedAccounts = $this->batchSyncAccountsForContacts($missingCompanyIds);
$mappings = array_merge($mappings, $syncedAccounts);
} catch (\Throwable $e) {
$this->logger->error('[' . $this->getDisplayName() . '] Failed to batch sync missing accounts', [
'teamId' => $this->team->getId(),
'missingCompanyIds' => $missingCompanyIds,
'missingCount' => count($missingCompanyIds),
'error' => $e->getMessage(),
]);
}
return $mappings;
}
private function batchSyncAccountsForContacts(array $companyIds): array
{
$syncedAccounts = [];
$fields = $this->getCompanyFields();
foreach (array_chunk($companyIds, self::BATCH_SIZE) as $chunk) {
try {
$companies = $this->client->getCompaniesByIds($chunk, $fields);
foreach ($companies as $companyData) {
try {
$account = $this->importAccount($companyData);
if ($account) {
$syncedAccounts[$account->getCrmProviderId()] = $account->getId();
}
} catch (\Throwable $e) {
$this->logger->warning('[' . $this->getDisplayName() . '] Failed to import account in batch', [
'teamId' => $this->team->getId(),
'companyId' => $companyData['id'] ?? 'unknown',
'error' => $e->getMessage(),
]);
}
}
} catch (\Throwable $e) {
$this->logger->warning('[' . $this->getDisplayName() . '] Batch company fetch failed', [
'teamId' => $this->team->getId(),
'chunk_size' => \count($chunk),
'error' => $e->getMessage(),
]);
}
}
return $syncedAccounts;
}
/**
* Process webhook-collected company batches.
*
* Drains Redis sets containing company CRM IDs collected from webhook events
* and dispatches ImportAccountBatch jobs for batch processing.
*
* @return int Number of company IDs dispatched to jobs
*/
public function batchSyncCompanies(): int
{
$configId = $this->team->getCrmConfiguration()->getId();
return $this->batchProcessor->processBatchesForObjectType(
WebhookSyncBatchProcessor::OBJECT_TYPE_COMPANY,
$configId
);
}
public function importAccountBatchByIds(array $crmIds): array
{
$result = [
'success_count' => 0,
'failed_ids' => [],
'errors' => [],
];
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$batchStart = microtime(true);
$slowAccounts = [];
$fields = $this->getCompanyFields();
$allCompanies = [];
$fetchStart = microtime(true);
foreach (array_chunk($crmIds, self::BATCH_SIZE) as $chunk) {
try {
$companies = $this->client->getCompaniesByIds($chunk, $fields);
foreach ($companies as $companyData) {
$allCompanies[] = $companyData;
}
} catch (\Throwable $e) {
$this->logger->warning('[' . $this->getDisplayName() . '] Batch company fetch failed', [
'chunk_size' => \count($chunk),
'error' => $e->getMessage(),
]);
throw $e;
}
}
$fetchMs = (int) round((microtime(true) - $fetchStart) * 1000);
$fetchedIds = array_map('strval', array_column($allCompanies, 'id'));
$notFoundIds = array_values(array_diff(array_map('strval', $crmIds), $fetchedIds));
if (! empty($notFoundIds)) {
$this->logger->info('[' . $this->getDisplayName() . '] Company CRM IDs not found in HubSpot', [
'teamId' => $this->team->getId(),
'notFoundCount' => \count($notFoundIds),
'notFoundIds' => $notFoundIds,
'requestedCount' => \count($crmIds),
'fetchedCount' => \count($allCompanies),
]);
}
$loopStart = microtime(true);
foreach ($allCompanies as $companyData) {
$accountStart = microtime(true);
try {
$account = $this->importAccount($companyData);
if ($account !== null) {
$result['success_count']++;
}
} catch (\Throwable $e) {
$crmId = $companyData['id'] ?? $companyData['properties']['hs_object_id'] ?? 'unknown';
$result['failed_ids'][] = $crmId;
$result['errors'][$crmId] = $e->getMessage();
$this->logger->warning('[' . $this->getDisplayName() . '] Failed to import account', [
'crmId' => $crmId,
'error' => $e->getMessage(),
]);
}
$accountMs = (int) round((microtime(true) - $accountStart) * 1000);
if ($accountMs > 1000) {
$crmId = $companyData['id'] ?? $companyData['properties']['hs_object_id'] ?? 'unknown';
$slowAccounts[] = ['crmId' => $crmId, 'ms' => $accountMs];
}
}
$loopMs = (int) round((microtime(true) - $loopStart) * 1000);
$totalMs = (int) round((microtime(true) - $batchStart) * 1000);
$this->logger->info('[' . $this->getDisplayName() . '] importAccountBatch timing', [
'teamId' => $this->team->getId(),
'account_count' => \count($allCompanies),
'requested_count' => \count($crmIds),
'not_found_count' => \count($notFoundIds),
'total_ms' => $totalMs,
'fetch_api_ms' => $fetchMs,
'accounts_loop_ms' => $loopMs,
'avg_account_ms' => ! empty($allCompanies) ? (int) round($loopMs / \count($allCompanies)) : 0,
'slow_accounts_count' => \count($slowAccounts),
'slow_accounts' => array_slice($slowAccounts, 0, 10),
]);
return $result;
}
private function getCompanyFields(): array
{
return [
'country',
'name',
'phone',
'domain',
'hs_avatar_filemanager_key',
'industry',
'hubspot_owner_id',
'hs_object_id',
'createdate',
'hs_lastmodifieddate',
];
}
private function importAccount($crmData): ?Account
{
$crmProviderId = $crmData['id'] ?? $crmData['properties']['hs_object_id'] ?? null;
$this->logger->info('[HubSpot] importAccount', [
'crm_provider_id' => $crmProviderId,
'config_id' => $this->config->getId(),
]);
if (empty($crmData['properties'])) {
$this->logger->info('[HubSpot] importAccount failed: empty properties', [
'crm_provider_id' => $crmProviderId,
'config_id' => $this->config->getId(),
]);
return null;
}
$properties = $crmData['properties'];
$crmId = (string) $properties['hs_object_id'];
$countryCode = isset($properties['country']) ? $this->convertCountryNameToCode($properties['country']) : null;
if (isset($properties['phone'])) {
// Trim to our width and attempt to parse it.
$number = mb_strimwidth($properties['phone'], 0, 25);
$parsedNumber = parsePhoneNumber($countryCode, $number);
} else {
$parsedNumber = [];
}
$name = '[unknown]';
if (isset($properties['name'])) {
$name = $properties['name'];
}
$photoPath = $this->prospectPhotoPathService->getOrGeneratePhotoPath(
$this->config,
$crmId,
Account::class,
$crmId,
$name
);
$industry = null;
if (isset($properties['industry'])) {
$industry = mb_strimwidth($properties['industry'], 0, 40);
}
$ownerId = $profile = null;
if (isset($properties['hubspot_owner_id'])) {
$ownerId = $properties['hubspot_owner_id'];
$profile = $this->crmEntityRepository->findProfileByExternalId($this->config, (string) $ownerId);
}
$domain = null;
if (isset($properties['domain'])) {
$domain = StringUtil::resolveDomain($properties['domain']);
}
$remotelyCreatedAt = null;
if (isset($properties['createdate']) && ! empty($properties['createdate'])) {
$remotelyCreatedAt = Carbon::parse($properties['createdate']);
}
$data = [
'crm_provider_id' => $crmId,
'team_id' => $this->team->id,
'user_id' => $profile ? $profile->user_id : null,
'owner_id' => $ownerId,
'name' => mb_strimwidth($name, 0, 191),
'photo_path' => $photoPath,
'industry' => $industry,
'domain' => $domain !== null
? substr($domain, 0, 191)
: null,
'phone' => $parsedNumber['phone'] ?? null,
'ext' => $parsedNumber['ext'] ?? null,
'country_code' => $countryCode,
'remotely_created_at' => $remotelyCreatedAt,
];
return $this->crmEntityRepository->importAccount($this->config, $data);
}
public function deleteContact(string $crmProviderId): bool
{
try {
$contact = $this->crmEntityRepository->findContactByExternalId($this->config, $crmProviderId);
if (! $contact) {
$this->logger->info('[HubSpot] Contact not found for deletion', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
// return success because we do not know which instance is the target
return true;
}
$id = $contact->getId();
$this->logger->info('[HubSpot] Deleting contact via webhook', [
'contact_id' => $id,
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
$contact->delete();
DeleteContactJob::dispatch($id)->afterCommit();
return true;
} catch (Exception $e) {
$this->logger->error('[HubSpot] Failed to delete contact via webhook', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
'error' => $e->getMessage(),
]);
return false;
}
}
public function deleteAccount(string $crmProviderId): bool
{
try {
$account = $this->crmEntityRepository->findAccountByExternalId($this->config, $crmProviderId);
if (! $account) {
$this->logger->info('[HubSpot] Account not found for deletion', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
// return success because we do not know which instance is the target
return true;
}
$id = $account->getId();
$this->logger->info('[HubSpot] Deleting account via webhook', [
'account_id' => $id,
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
$account->delete();
DeleteAccountJob::dispatch($id)->afterCommit();
return true;
} catch (Exception $e) {
$this->logger->error('[HubSpot] Failed to delete account via webhook', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
'error' => $e->getMessage(),
]);
return false;
}
}
public function deleteOpportunity(string $crmProviderId): bool
{
try {
$opportunity = $this->crmEntityRepository->findOpportunityByExternalId($this->config, $crmProviderId);
if (! $opportunity) {
$this->logger->info('[HubSpot] Opportunity not found for deletion', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
// return success because we do not know which instance is the target
return true;
}
$id = $opportunity->getId();
$this->logger->info('[HubSpot] Deleting opportunity via webhook', [
'opportunity_id' => $id,
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
$opportunity->delete();
DeleteOpportunityJob::dispatch($id)->afterCommit();
return true;
} catch (Exception $e) {
$this->logger->error('[HubSpot] Failed to delete opportunity via webhook', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
'error' => $e->getMessage(),
]);
return false;
}
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.034242023,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: master","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"Editor for custom.log","depth":4,"bounds":{"left":0.5475399,"top":0.0726257,"width":0.44082448,"height":0.9066241},"on_screen":true,"role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits;\n\nuse Carbon\\Carbon;\nuse Exception;\nuse Illuminate\\Support\\Str;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Jobs\\Crm\\Delete\\DeleteAccountJob;\nuse Jiminny\\Jobs\\Crm\\Delete\\DeleteContactJob;\nuse Jiminny\\Jobs\\Crm\\Delete\\DeleteOpportunityJob;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Repositories\\Crm\\CrmEntityRepository;\nuse Jiminny\\Services\\Avatar\\ProspectPhotoPathService;\nuse Jiminny\\Services\\Crm\\Hubspot\\HubspotClientInterface;\nuse Jiminny\\Services\\Crm\\Hubspot\\WebhookSyncBatchProcessor;\nuse Jiminny\\Utils\\StringUtil;\n\ntrait SyncCrmEntitiesTrait\n{\n use OpportunitySyncTrait;\n private const string CDN_URL = 'https://cdn2.hubspot.net/';\n protected CrmEntityRepository $crmEntityRepository;\n protected ProspectPhotoPathService $prospectPhotoPathService;\n\n private function getAssociationDataForCollection(array $collection, string $fromObject, string $toObject): array\n {\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $hsOpportunityIds = array_column($collection, 'id');\n\n return $this->client->getAssociationsData($hsOpportunityIds, $fromObject, $toObject);\n }\n\n private function importAssociationData(array $collection, array $associatedData): array\n {\n $data = [];\n if (! empty($associatedData[$collection['id']])) {\n foreach ($associatedData[$collection['id']] as $id) {\n $data[] = [\n 'id' => $id,\n ];\n }\n }\n\n return ['results' => $data];\n }\n\n /**\n * Sync contacts modified since a given date (manual sync mode).\n *\n * This method fetches contacts from HubSpot API based on modification date and\n * imports them one by one. It is used for:\n * - Manual sync commands (e.g., crm:sync-contact with --from parameter)\n * - Initial sync for new teams\n * - Backfill operations\n *\n * For regular sync webhook batchSyncContacts is used:\n *\n * @param Carbon $since Fetch contacts modified after this date\n * @param Carbon|null $to Optional end date for modification range\n *\n * @return int Number of contacts successfully synced\n */\n public function syncContacts(Carbon $since, ?Carbon $to = null): int\n {\n $syncCount = 0;\n\n try {\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $fields = $this->getContactFields();\n $payload = $this->payloadBuilder->getRecentlyUpdatedSearchPayload($since, $to, $fields);\n\n foreach ($this->client->getPaginatedDataGenerator($payload, 'contacts') as $hsContact) {\n $this->importContact($hsContact);\n $syncCount++;\n }\n } catch (Exception $exception) {\n $this->logger->error('[' . $this->getDisplayName() . '] Sync contacts failed', [\n 'teamId' => $this->team->getUuid(),\n 'reason' => $exception->getMessage(),\n ]);\n }\n\n return $syncCount;\n }\n\n /**\n * @inheritdoc\n */\n public function syncContact(string $crmId): ?Contact\n {\n try {\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $fields = $this->getContactFields();\n $hsContact = $this->client->getContactById($crmId, $fields);\n } catch (\\HubSpot\\Client\\Crm\\Contacts\\ApiException $e) {\n $this->logger->info('[' . $this->getDisplayName() . '] Contact fetch failed', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n return null;\n } catch (CrmException $e) {\n $this->logger->info('[' . $this->getDisplayName() . '] Contact not found', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n return null;\n }\n\n if (empty($hsContact['properties']) || empty($hsContact['id'])) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Contact data incomplete', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'has_properties' => ! empty($hsContact['properties']),\n 'has_id' => ! empty($hsContact['id']),\n ]);\n\n return null;\n }\n\n return $this->importContact($hsContact);\n }\n\n private function getContactFields(): array\n {\n return [\n 'associatedcompanyid',\n 'country',\n 'firstname',\n 'lastname',\n 'phone',\n 'mobilephone',\n 'email',\n 'photo',\n 'hs_avatar_filemanager_key',\n 'jobtitle',\n 'hubspot_owner_id',\n 'hs_object_id',\n 'createdate',\n 'hs_lastmodifieddate',\n ];\n }\n\n /**\n * @inheritdoc\n */\n private function importContact($crmData, array $accountMappings = []): ?Contact\n {\n $crmProviderId = $crmData['id'] ?? null;\n\n $this->logger->info('[HubSpot] importContact', [\n 'crm_provider_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n if (empty($crmData['properties'])) {\n $this->logger->info('[HubSpot] importContact failed: empty properties', [\n 'crm_provider_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n return null;\n }\n\n $properties = $crmData['properties'];\n $crmId = (string) $crmData['id'];\n\n $accountId = $this->resolveContactAccount($properties, $accountMappings);\n $data = $this->buildContactData($crmId, $properties, $accountId);\n\n return $this->crmEntityRepository->importContact($this->config, $data);\n }\n\n private function resolveContactAccount(array $properties, array $accountMappings): ?int\n {\n if (empty($properties['associatedcompanyid'])) {\n return null;\n }\n\n $companyId = (string) $properties['associatedcompanyid'];\n\n if (! empty($accountMappings)) {\n return $accountMappings[$companyId] ?? null;\n }\n\n return $this->crmEntityRepository->findAccountByExternalId(\n $this->team->getCrmConfiguration(),\n $companyId\n )?->getId() ?? $this->syncAccount($companyId)?->getId();\n }\n\n private function buildContactData(string $crmId, array $properties, ?int $accountId): array\n {\n $countryCode = $this->buildContactCountry($properties);\n $name = $this->buildContactName($properties);\n $photoPath = $this->teamService->generateAvatar(\n $crmId,\n empty($name) ? ($properties['email'] ?? 'N/A') : $name,\n );\n $parsedNumber = $this->buildContactPhone($countryCode, $properties);\n $mobileNumber = $this->buildContactMobilePhone($countryCode, $properties);\n\n $ownerId = $properties['hubspot_owner_id'] ?? null;\n $profile = $ownerId !== null\n ? $this->crmEntityRepository->findProfileByExternalId($this->config, (string) $ownerId)\n : null;\n\n $ext = (isset($parsedNumber['ext']) && is_string($parsedNumber['ext']) && strlen($parsedNumber['ext']) <= 10)\n ? $parsedNumber['ext']\n : null;\n\n $title = isset($properties['jobtitle']) ? mb_strimwidth($properties['jobtitle'], 0, 128) : null;\n $email = isset($properties['email']) ? mb_strimwidth($properties['email'], 0, 191) : null;\n $remotelyCreatedAt = ! empty($properties['createdate']) ? Carbon::parse($properties['createdate']) : null;\n\n return [\n 'crm_provider_id' => $crmId,\n 'team_id' => $this->team->getId(),\n 'account_id' => $accountId,\n 'user_id' => $profile?->getUserId(),\n 'owner_id' => $ownerId,\n 'name' => $name,\n 'title' => $title,\n 'email' => $email,\n 'country_code' => $countryCode,\n 'phone' => $parsedNumber['phone'] ?? null,\n 'mobile_phone' => $mobileNumber ?? null,\n 'ext' => $ext,\n 'photo_path' => $photoPath,\n 'remotely_created_at' => $remotelyCreatedAt,\n ];\n }\n\n /**\n * @param $properties\n */\n private function buildContactName($properties): string\n {\n if (is_array($properties)) {\n return $this->buildContactNameFromArray($properties);\n }\n\n return $this->buildContactNameFromObject($properties);\n }\n\n private function buildContactNameFromArray(array $properties): string\n {\n if (! empty($properties['name'])) {\n return mb_strimwidth($properties['name'], 0, 100);\n }\n\n $name = '';\n if (! empty($properties['firstname'])) {\n $name = $properties['firstname'] . ' ';\n }\n\n if (! empty($properties['lastname'])) {\n $name .= $properties['lastname'];\n }\n\n if ($name === '' && ! empty($properties['email'])) {\n $name = $properties['email'];\n }\n\n return mb_strimwidth($name, 0, 100);\n }\n\n private function buildContactNameFromObject($properties): string\n {\n $name = '';\n if (isset($properties->firstname)) {\n $name = $properties->firstname->value . ' ';\n }\n if (isset($properties->lastname)) {\n $name .= $properties->lastname->value;\n }\n if ($name === '' && isset($properties->email)) {\n $name = $properties->email->value;\n }\n\n return mb_strimwidth($name, 0, 100);\n }\n\n /**\n * @param $properties\n */\n private function buildContactPhone(?string $countryCode, $properties): ?array\n {\n if (is_array($properties) && empty($properties['phone']) === false) {\n $number = mb_strimwidth($properties['phone'], 0, 25);\n\n return parsePhoneNumber($countryCode, $number);\n } elseif (isset($properties->phone)) {\n $number = mb_strimwidth($properties->phone->value, 0, 25);\n\n return parsePhoneNumber($countryCode, $number);\n }\n\n return [];\n }\n\n /**\n * @param $properties\n */\n private function buildContactMobilePhone(?string $countryCode, $properties): ?string\n {\n return isset($properties['mobilephone'])\n ? Str::limit(phone_e164($countryCode, $properties['mobilephone']), 25, '')\n : null;\n }\n\n /**\n * @param $properties\n * @param $account\n */\n private function buildContactCountry($properties): ?string\n {\n if (is_array($properties) && empty($properties['country']) === false) {\n return $this->convertCountryNameToCode($properties['country']);\n }\n\n if (isset($properties->country)) {\n return $this->convertCountryNameToCode($properties->country->value);\n }\n\n return null;\n }\n\n /**\n * HubSpot doesn't have leads, so this method does nothing.\n *\n * @param Carbon $since\n * @param Carbon|null $to\n * @param string|null $crmProfileId\n *\n * @return int\n */\n public function syncLeads(Carbon $since, ?Carbon $to = null, ?string $crmProfileId = null): int\n {\n // Mark unused parameters to avoid code smell warnings\n unset($since, $to, $crmProfileId);\n\n return 0;\n }\n\n /**\n * HubSpot doesn't have leads.\n *\n * @param string $crmId\n *\n * @inheritdoc\n */\n public function syncLead(string $crmId): ?Lead\n {\n // Mark unused parameter to avoid code smell warnings\n unset($crmId);\n\n return null;\n }\n\n /**\n * Sync accounts (companies) modified since a given date (manual sync mode).\n *\n * This method fetches companies from HubSpot API based on modification date and\n * imports them one by one. It is used for:\n * - Manual sync commands (e.g., crm:sync-account with --from parameter)\n * - Initial sync for new teams\n * - Backfill operations\n *\n * For regular sync webhook batchSyncCompanies is used:\n *\n * @param Carbon $since Fetch companies modified after this date\n * @param Carbon|null $to Optional end date for modification range\n *\n * @return int Number of companies successfully synced\n */\n public function syncAccounts(Carbon $since, ?Carbon $to = null): int\n {\n $syncCount = 0;\n\n try {\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $fields = $this->getCompanyFields();\n $payload = $this->payloadBuilder->getRecentlyUpdatedSearchPayload($since, $to, $fields);\n\n foreach ($this->client->getPaginatedDataGenerator($payload, 'companies') as $hsAccount) {\n $this->importAccount($hsAccount);\n $syncCount++;\n }\n } catch (Exception $exception) {\n $this->logger->error('[' . $this->getDisplayName() . '] Sync accounts failed', [\n 'teamId' => $this->team->getUuid(),\n 'reason' => $exception->getMessage(),\n ]);\n }\n\n return $syncCount;\n }\n\n /**\n * @inheritdoc\n */\n public function syncAccount(string $crmId): ?Account\n {\n try {\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $fields = $this->getCompanyFields();\n $hsAccount = $this->client->getAccountById($crmId, $fields);\n } catch (\\HubSpot\\Client\\Crm\\Companies\\ApiException $e) {\n $this->logger->info('[' . $this->getDisplayName() . '] Account fetch failed', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n return null;\n } catch (CrmException $e) {\n $this->logger->info('[' . $this->getDisplayName() . '] Account not found', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n return null;\n }\n\n return $this->importAccount($hsAccount);\n }\n\n /**\n * Process webhook-collected contact batches.\n *\n * Drains Redis sets containing contact CRM IDs collected from webhook events\n * and dispatches ImportContactBatch jobs for batch processing.\n *\n * @return int Number of contact IDs dispatched to jobs\n */\n public function batchSyncContacts(): int\n {\n $configId = $this->team->getCrmConfiguration()->getId();\n\n return $this->batchProcessor->processBatchesForObjectType(\n WebhookSyncBatchProcessor::OBJECT_TYPE_CONTACT,\n $configId\n );\n }\n\n public function importContactBatchByIds(array $crmIds): array\n {\n $result = [\n 'success_count' => 0,\n 'failed_ids' => [],\n 'errors' => [],\n ];\n\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $batchStart = microtime(true);\n $slowContacts = [];\n\n $fetchStart = microtime(true);\n $allContacts = $this->fetchContactsByIdsInChunks($crmIds);\n $fetchMs = (int) round((microtime(true) - $fetchStart) * 1000);\n\n $fetchedIds = array_map('strval', array_column($allContacts, 'id'));\n $notFoundIds = array_values(array_diff(array_map('strval', $crmIds), $fetchedIds));\n\n if (! empty($notFoundIds)) {\n $this->logger->info('[' . $this->getDisplayName() . '] Contact CRM IDs not found in HubSpot', [\n 'teamId' => $this->team->getId(),\n 'notFoundCount' => \\count($notFoundIds),\n 'notFoundIds' => $notFoundIds,\n 'requestedCount' => \\count($crmIds),\n 'fetchedCount' => \\count($allContacts),\n ]);\n }\n\n if (empty($allContacts)) {\n return $result;\n }\n\n $prepareStart = microtime(true);\n $accountMappings = $this->prepareAccountMappingsForContacts($allContacts);\n $prepareAccountsMs = (int) round((microtime(true) - $prepareStart) * 1000);\n\n $loopStart = microtime(true);\n foreach ($allContacts as $contactData) {\n $contactStart = microtime(true);\n\n try {\n $contact = $this->importContact($contactData, $accountMappings);\n if ($contact !== null) {\n $result['success_count']++;\n }\n } catch (\\Throwable $e) {\n $crmId = $contactData['id'] ?? 'unknown';\n $result['failed_ids'][] = $crmId;\n $result['errors'][$crmId] = $e->getMessage();\n\n $this->logger->warning('[' . $this->getDisplayName() . '] Failed to import contact', [\n 'teamId' => $this->team->getId(),\n 'crmId' => $crmId,\n 'error' => $e->getMessage(),\n ]);\n }\n\n $contactMs = (int) round((microtime(true) - $contactStart) * 1000);\n if ($contactMs > 1000) {\n $slowContacts[] = ['crmId' => $contactData['id'] ?? 'unknown', 'ms' => $contactMs];\n }\n }\n $loopMs = (int) round((microtime(true) - $loopStart) * 1000);\n $totalMs = (int) round((microtime(true) - $batchStart) * 1000);\n\n $this->logger->info('[' . $this->getDisplayName() . '] importContactBatch timing', [\n 'teamId' => $this->team->getId(),\n 'contact_count' => \\count($allContacts),\n 'requested_count' => \\count($crmIds),\n 'not_found_count' => \\count($notFoundIds),\n 'total_ms' => $totalMs,\n 'fetch_api_ms' => $fetchMs,\n 'prepare_accounts_ms' => $prepareAccountsMs,\n 'contacts_loop_ms' => $loopMs,\n 'avg_contact_ms' => ! empty($allContacts) ? (int) round($loopMs / \\count($allContacts)) : 0,\n 'slow_contacts_count' => \\count($slowContacts),\n 'slow_contacts' => array_slice($slowContacts, 0, 10),\n ]);\n\n return $result;\n }\n\n private function fetchContactsByIdsInChunks(array $crmIds): array\n {\n $fields = $this->getContactFields();\n $allContacts = [];\n\n foreach (array_chunk($crmIds, self::BATCH_SIZE) as $chunk) {\n try {\n $contacts = $this->client->getContactsByIds($chunk, $fields);\n foreach ($contacts as $contactData) {\n $allContacts[] = $contactData;\n }\n } catch (\\Throwable $e) {\n // @TODO what will happen if this exception is thrown\n $this->logger->warning('[' . $this->getDisplayName() . '] Batch contact fetch failed', [\n 'chunk_size' => \\count($chunk),\n 'error' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n }\n\n return $allContacts;\n }\n\n private function prepareAccountMappingsForContacts(array $contacts): array\n {\n $companyIds = [];\n foreach ($contacts as $contact) {\n $companyId = $contact['properties']['associatedcompanyid'] ?? null;\n if ($companyId !== null && $companyId !== '') {\n $companyIds[] = (string) $companyId;\n }\n }\n\n $companyIds = array_unique($companyIds);\n\n if (empty($companyIds)) {\n return [];\n }\n\n $mappings = $this->crmEntityRepository\n ->getExistingAccountIdsMap($this->config, $companyIds);\n\n $missingCompanyIds = array_diff($companyIds, array_keys($mappings));\n\n if (empty($missingCompanyIds)) {\n return $mappings;\n }\n\n $this->logger->info('[' . $this->getDisplayName() . '] Batch syncing missing accounts for contacts', [\n 'teamId' => $this->team->getId(),\n 'total_companies' => \\count($companyIds),\n 'existing_companies' => \\count($mappings),\n 'missing_companies' => \\count($missingCompanyIds),\n ]);\n\n try {\n $syncedAccounts = $this->batchSyncAccountsForContacts($missingCompanyIds);\n $mappings = array_merge($mappings, $syncedAccounts);\n } catch (\\Throwable $e) {\n $this->logger->error('[' . $this->getDisplayName() . '] Failed to batch sync missing accounts', [\n 'teamId' => $this->team->getId(),\n 'missingCompanyIds' => $missingCompanyIds,\n 'missingCount' => count($missingCompanyIds),\n 'error' => $e->getMessage(),\n ]);\n }\n\n return $mappings;\n }\n\n private function batchSyncAccountsForContacts(array $companyIds): array\n {\n $syncedAccounts = [];\n $fields = $this->getCompanyFields();\n\n foreach (array_chunk($companyIds, self::BATCH_SIZE) as $chunk) {\n try {\n $companies = $this->client->getCompaniesByIds($chunk, $fields);\n\n foreach ($companies as $companyData) {\n try {\n $account = $this->importAccount($companyData);\n if ($account) {\n $syncedAccounts[$account->getCrmProviderId()] = $account->getId();\n }\n } catch (\\Throwable $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Failed to import account in batch', [\n 'teamId' => $this->team->getId(),\n 'companyId' => $companyData['id'] ?? 'unknown',\n 'error' => $e->getMessage(),\n ]);\n }\n }\n } catch (\\Throwable $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Batch company fetch failed', [\n 'teamId' => $this->team->getId(),\n 'chunk_size' => \\count($chunk),\n 'error' => $e->getMessage(),\n ]);\n }\n }\n\n return $syncedAccounts;\n }\n\n /**\n * Process webhook-collected company batches.\n *\n * Drains Redis sets containing company CRM IDs collected from webhook events\n * and dispatches ImportAccountBatch jobs for batch processing.\n *\n * @return int Number of company IDs dispatched to jobs\n */\n public function batchSyncCompanies(): int\n {\n $configId = $this->team->getCrmConfiguration()->getId();\n\n return $this->batchProcessor->processBatchesForObjectType(\n WebhookSyncBatchProcessor::OBJECT_TYPE_COMPANY,\n $configId\n );\n }\n\n public function importAccountBatchByIds(array $crmIds): array\n {\n $result = [\n 'success_count' => 0,\n 'failed_ids' => [],\n 'errors' => [],\n ];\n\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $batchStart = microtime(true);\n $slowAccounts = [];\n\n $fields = $this->getCompanyFields();\n $allCompanies = [];\n\n $fetchStart = microtime(true);\n foreach (array_chunk($crmIds, self::BATCH_SIZE) as $chunk) {\n try {\n $companies = $this->client->getCompaniesByIds($chunk, $fields);\n foreach ($companies as $companyData) {\n $allCompanies[] = $companyData;\n }\n } catch (\\Throwable $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Batch company fetch failed', [\n 'chunk_size' => \\count($chunk),\n 'error' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n }\n $fetchMs = (int) round((microtime(true) - $fetchStart) * 1000);\n\n $fetchedIds = array_map('strval', array_column($allCompanies, 'id'));\n $notFoundIds = array_values(array_diff(array_map('strval', $crmIds), $fetchedIds));\n\n if (! empty($notFoundIds)) {\n $this->logger->info('[' . $this->getDisplayName() . '] Company CRM IDs not found in HubSpot', [\n 'teamId' => $this->team->getId(),\n 'notFoundCount' => \\count($notFoundIds),\n 'notFoundIds' => $notFoundIds,\n 'requestedCount' => \\count($crmIds),\n 'fetchedCount' => \\count($allCompanies),\n ]);\n }\n\n $loopStart = microtime(true);\n foreach ($allCompanies as $companyData) {\n $accountStart = microtime(true);\n\n try {\n $account = $this->importAccount($companyData);\n if ($account !== null) {\n $result['success_count']++;\n }\n } catch (\\Throwable $e) {\n $crmId = $companyData['id'] ?? $companyData['properties']['hs_object_id'] ?? 'unknown';\n $result['failed_ids'][] = $crmId;\n $result['errors'][$crmId] = $e->getMessage();\n\n $this->logger->warning('[' . $this->getDisplayName() . '] Failed to import account', [\n 'crmId' => $crmId,\n 'error' => $e->getMessage(),\n ]);\n }\n\n $accountMs = (int) round((microtime(true) - $accountStart) * 1000);\n if ($accountMs > 1000) {\n $crmId = $companyData['id'] ?? $companyData['properties']['hs_object_id'] ?? 'unknown';\n $slowAccounts[] = ['crmId' => $crmId, 'ms' => $accountMs];\n }\n }\n $loopMs = (int) round((microtime(true) - $loopStart) * 1000);\n $totalMs = (int) round((microtime(true) - $batchStart) * 1000);\n\n $this->logger->info('[' . $this->getDisplayName() . '] importAccountBatch timing', [\n 'teamId' => $this->team->getId(),\n 'account_count' => \\count($allCompanies),\n 'requested_count' => \\count($crmIds),\n 'not_found_count' => \\count($notFoundIds),\n 'total_ms' => $totalMs,\n 'fetch_api_ms' => $fetchMs,\n 'accounts_loop_ms' => $loopMs,\n 'avg_account_ms' => ! empty($allCompanies) ? (int) round($loopMs / \\count($allCompanies)) : 0,\n 'slow_accounts_count' => \\count($slowAccounts),\n 'slow_accounts' => array_slice($slowAccounts, 0, 10),\n ]);\n\n return $result;\n }\n\n private function getCompanyFields(): array\n {\n return [\n 'country',\n 'name',\n 'phone',\n 'domain',\n 'hs_avatar_filemanager_key',\n 'industry',\n 'hubspot_owner_id',\n 'hs_object_id',\n 'createdate',\n 'hs_lastmodifieddate',\n ];\n }\n\n private function importAccount($crmData): ?Account\n {\n $crmProviderId = $crmData['id'] ?? $crmData['properties']['hs_object_id'] ?? null;\n\n $this->logger->info('[HubSpot] importAccount', [\n 'crm_provider_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n if (empty($crmData['properties'])) {\n $this->logger->info('[HubSpot] importAccount failed: empty properties', [\n 'crm_provider_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n return null;\n }\n\n $properties = $crmData['properties'];\n $crmId = (string) $properties['hs_object_id'];\n\n $countryCode = isset($properties['country']) ? $this->convertCountryNameToCode($properties['country']) : null;\n\n if (isset($properties['phone'])) {\n // Trim to our width and attempt to parse it.\n $number = mb_strimwidth($properties['phone'], 0, 25);\n $parsedNumber = parsePhoneNumber($countryCode, $number);\n } else {\n $parsedNumber = [];\n }\n\n $name = '[unknown]';\n if (isset($properties['name'])) {\n $name = $properties['name'];\n }\n\n $photoPath = $this->prospectPhotoPathService->getOrGeneratePhotoPath(\n $this->config,\n $crmId,\n Account::class,\n $crmId,\n $name\n );\n\n $industry = null;\n if (isset($properties['industry'])) {\n $industry = mb_strimwidth($properties['industry'], 0, 40);\n }\n\n $ownerId = $profile = null;\n if (isset($properties['hubspot_owner_id'])) {\n $ownerId = $properties['hubspot_owner_id'];\n $profile = $this->crmEntityRepository->findProfileByExternalId($this->config, (string) $ownerId);\n }\n\n $domain = null;\n if (isset($properties['domain'])) {\n $domain = StringUtil::resolveDomain($properties['domain']);\n }\n\n $remotelyCreatedAt = null;\n if (isset($properties['createdate']) && ! empty($properties['createdate'])) {\n $remotelyCreatedAt = Carbon::parse($properties['createdate']);\n }\n\n $data = [\n 'crm_provider_id' => $crmId,\n 'team_id' => $this->team->id,\n 'user_id' => $profile ? $profile->user_id : null,\n 'owner_id' => $ownerId,\n 'name' => mb_strimwidth($name, 0, 191),\n 'photo_path' => $photoPath,\n 'industry' => $industry,\n 'domain' => $domain !== null\n ? substr($domain, 0, 191)\n : null,\n 'phone' => $parsedNumber['phone'] ?? null,\n 'ext' => $parsedNumber['ext'] ?? null,\n 'country_code' => $countryCode,\n 'remotely_created_at' => $remotelyCreatedAt,\n ];\n\n return $this->crmEntityRepository->importAccount($this->config, $data);\n }\n\n public function deleteContact(string $crmProviderId): bool\n {\n try {\n $contact = $this->crmEntityRepository->findContactByExternalId($this->config, $crmProviderId);\n\n if (! $contact) {\n $this->logger->info('[HubSpot] Contact not found for deletion', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n // return success because we do not know which instance is the target\n return true;\n }\n\n $id = $contact->getId();\n\n $this->logger->info('[HubSpot] Deleting contact via webhook', [\n 'contact_id' => $id,\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n $contact->delete();\n DeleteContactJob::dispatch($id)->afterCommit();\n\n return true;\n } catch (Exception $e) {\n $this->logger->error('[HubSpot] Failed to delete contact via webhook', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n return false;\n }\n }\n\n public function deleteAccount(string $crmProviderId): bool\n {\n try {\n $account = $this->crmEntityRepository->findAccountByExternalId($this->config, $crmProviderId);\n\n if (! $account) {\n $this->logger->info('[HubSpot] Account not found for deletion', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n // return success because we do not know which instance is the target\n return true;\n }\n\n $id = $account->getId();\n\n $this->logger->info('[HubSpot] Deleting account via webhook', [\n 'account_id' => $id,\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n $account->delete();\n DeleteAccountJob::dispatch($id)->afterCommit();\n\n return true;\n } catch (Exception $e) {\n $this->logger->error('[HubSpot] Failed to delete account via webhook', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n return false;\n }\n }\n\n public function deleteOpportunity(string $crmProviderId): bool\n {\n try {\n $opportunity = $this->crmEntityRepository->findOpportunityByExternalId($this->config, $crmProviderId);\n\n if (! $opportunity) {\n $this->logger->info('[HubSpot] Opportunity not found for deletion', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n // return success because we do not know which instance is the target\n return true;\n }\n\n $id = $opportunity->getId();\n\n $this->logger->info('[HubSpot] Deleting opportunity via webhook', [\n 'opportunity_id' => $id,\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n $opportunity->delete();\n DeleteOpportunityJob::dispatch($id)->afterCommit();\n\n return true;\n } catch (Exception $e) {\n $this->logger->error('[HubSpot] Failed to delete opportunity via webhook', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n return false;\n }\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits;\n\nuse Carbon\\Carbon;\nuse Exception;\nuse Illuminate\\Support\\Str;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Jobs\\Crm\\Delete\\DeleteAccountJob;\nuse Jiminny\\Jobs\\Crm\\Delete\\DeleteContactJob;\nuse Jiminny\\Jobs\\Crm\\Delete\\DeleteOpportunityJob;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Repositories\\Crm\\CrmEntityRepository;\nuse Jiminny\\Services\\Avatar\\ProspectPhotoPathService;\nuse Jiminny\\Services\\Crm\\Hubspot\\HubspotClientInterface;\nuse Jiminny\\Services\\Crm\\Hubspot\\WebhookSyncBatchProcessor;\nuse Jiminny\\Utils\\StringUtil;\n\ntrait SyncCrmEntitiesTrait\n{\n use OpportunitySyncTrait;\n private const string CDN_URL = 'https://cdn2.hubspot.net/';\n protected CrmEntityRepository $crmEntityRepository;\n protected ProspectPhotoPathService $prospectPhotoPathService;\n\n private function getAssociationDataForCollection(array $collection, string $fromObject, string $toObject): array\n {\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $hsOpportunityIds = array_column($collection, 'id');\n\n return $this->client->getAssociationsData($hsOpportunityIds, $fromObject, $toObject);\n }\n\n private function importAssociationData(array $collection, array $associatedData): array\n {\n $data = [];\n if (! empty($associatedData[$collection['id']])) {\n foreach ($associatedData[$collection['id']] as $id) {\n $data[] = [\n 'id' => $id,\n ];\n }\n }\n\n return ['results' => $data];\n }\n\n /**\n * Sync contacts modified since a given date (manual sync mode).\n *\n * This method fetches contacts from HubSpot API based on modification date and\n * imports them one by one. It is used for:\n * - Manual sync commands (e.g., crm:sync-contact with --from parameter)\n * - Initial sync for new teams\n * - Backfill operations\n *\n * For regular sync webhook batchSyncContacts is used:\n *\n * @param Carbon $since Fetch contacts modified after this date\n * @param Carbon|null $to Optional end date for modification range\n *\n * @return int Number of contacts successfully synced\n */\n public function syncContacts(Carbon $since, ?Carbon $to = null): int\n {\n $syncCount = 0;\n\n try {\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $fields = $this->getContactFields();\n $payload = $this->payloadBuilder->getRecentlyUpdatedSearchPayload($since, $to, $fields);\n\n foreach ($this->client->getPaginatedDataGenerator($payload, 'contacts') as $hsContact) {\n $this->importContact($hsContact);\n $syncCount++;\n }\n } catch (Exception $exception) {\n $this->logger->error('[' . $this->getDisplayName() . '] Sync contacts failed', [\n 'teamId' => $this->team->getUuid(),\n 'reason' => $exception->getMessage(),\n ]);\n }\n\n return $syncCount;\n }\n\n /**\n * @inheritdoc\n */\n public function syncContact(string $crmId): ?Contact\n {\n try {\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $fields = $this->getContactFields();\n $hsContact = $this->client->getContactById($crmId, $fields);\n } catch (\\HubSpot\\Client\\Crm\\Contacts\\ApiException $e) {\n $this->logger->info('[' . $this->getDisplayName() . '] Contact fetch failed', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n return null;\n } catch (CrmException $e) {\n $this->logger->info('[' . $this->getDisplayName() . '] Contact not found', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n return null;\n }\n\n if (empty($hsContact['properties']) || empty($hsContact['id'])) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Contact data incomplete', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'has_properties' => ! empty($hsContact['properties']),\n 'has_id' => ! empty($hsContact['id']),\n ]);\n\n return null;\n }\n\n return $this->importContact($hsContact);\n }\n\n private function getContactFields(): array\n {\n return [\n 'associatedcompanyid',\n 'country',\n 'firstname',\n 'lastname',\n 'phone',\n 'mobilephone',\n 'email',\n 'photo',\n 'hs_avatar_filemanager_key',\n 'jobtitle',\n 'hubspot_owner_id',\n 'hs_object_id',\n 'createdate',\n 'hs_lastmodifieddate',\n ];\n }\n\n /**\n * @inheritdoc\n */\n private function importContact($crmData, array $accountMappings = []): ?Contact\n {\n $crmProviderId = $crmData['id'] ?? null;\n\n $this->logger->info('[HubSpot] importContact', [\n 'crm_provider_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n if (empty($crmData['properties'])) {\n $this->logger->info('[HubSpot] importContact failed: empty properties', [\n 'crm_provider_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n return null;\n }\n\n $properties = $crmData['properties'];\n $crmId = (string) $crmData['id'];\n\n $accountId = $this->resolveContactAccount($properties, $accountMappings);\n $data = $this->buildContactData($crmId, $properties, $accountId);\n\n return $this->crmEntityRepository->importContact($this->config, $data);\n }\n\n private function resolveContactAccount(array $properties, array $accountMappings): ?int\n {\n if (empty($properties['associatedcompanyid'])) {\n return null;\n }\n\n $companyId = (string) $properties['associatedcompanyid'];\n\n if (! empty($accountMappings)) {\n return $accountMappings[$companyId] ?? null;\n }\n\n return $this->crmEntityRepository->findAccountByExternalId(\n $this->team->getCrmConfiguration(),\n $companyId\n )?->getId() ?? $this->syncAccount($companyId)?->getId();\n }\n\n private function buildContactData(string $crmId, array $properties, ?int $accountId): array\n {\n $countryCode = $this->buildContactCountry($properties);\n $name = $this->buildContactName($properties);\n $photoPath = $this->teamService->generateAvatar(\n $crmId,\n empty($name) ? ($properties['email'] ?? 'N/A') : $name,\n );\n $parsedNumber = $this->buildContactPhone($countryCode, $properties);\n $mobileNumber = $this->buildContactMobilePhone($countryCode, $properties);\n\n $ownerId = $properties['hubspot_owner_id'] ?? null;\n $profile = $ownerId !== null\n ? $this->crmEntityRepository->findProfileByExternalId($this->config, (string) $ownerId)\n : null;\n\n $ext = (isset($parsedNumber['ext']) && is_string($parsedNumber['ext']) && strlen($parsedNumber['ext']) <= 10)\n ? $parsedNumber['ext']\n : null;\n\n $title = isset($properties['jobtitle']) ? mb_strimwidth($properties['jobtitle'], 0, 128) : null;\n $email = isset($properties['email']) ? mb_strimwidth($properties['email'], 0, 191) : null;\n $remotelyCreatedAt = ! empty($properties['createdate']) ? Carbon::parse($properties['createdate']) : null;\n\n return [\n 'crm_provider_id' => $crmId,\n 'team_id' => $this->team->getId(),\n 'account_id' => $accountId,\n 'user_id' => $profile?->getUserId(),\n 'owner_id' => $ownerId,\n 'name' => $name,\n 'title' => $title,\n 'email' => $email,\n 'country_code' => $countryCode,\n 'phone' => $parsedNumber['phone'] ?? null,\n 'mobile_phone' => $mobileNumber ?? null,\n 'ext' => $ext,\n 'photo_path' => $photoPath,\n 'remotely_created_at' => $remotelyCreatedAt,\n ];\n }\n\n /**\n * @param $properties\n */\n private function buildContactName($properties): string\n {\n if (is_array($properties)) {\n return $this->buildContactNameFromArray($properties);\n }\n\n return $this->buildContactNameFromObject($properties);\n }\n\n private function buildContactNameFromArray(array $properties): string\n {\n if (! empty($properties['name'])) {\n return mb_strimwidth($properties['name'], 0, 100);\n }\n\n $name = '';\n if (! empty($properties['firstname'])) {\n $name = $properties['firstname'] . ' ';\n }\n\n if (! empty($properties['lastname'])) {\n $name .= $properties['lastname'];\n }\n\n if ($name === '' && ! empty($properties['email'])) {\n $name = $properties['email'];\n }\n\n return mb_strimwidth($name, 0, 100);\n }\n\n private function buildContactNameFromObject($properties): string\n {\n $name = '';\n if (isset($properties->firstname)) {\n $name = $properties->firstname->value . ' ';\n }\n if (isset($properties->lastname)) {\n $name .= $properties->lastname->value;\n }\n if ($name === '' && isset($properties->email)) {\n $name = $properties->email->value;\n }\n\n return mb_strimwidth($name, 0, 100);\n }\n\n /**\n * @param $properties\n */\n private function buildContactPhone(?string $countryCode, $properties): ?array\n {\n if (is_array($properties) && empty($properties['phone']) === false) {\n $number = mb_strimwidth($properties['phone'], 0, 25);\n\n return parsePhoneNumber($countryCode, $number);\n } elseif (isset($properties->phone)) {\n $number = mb_strimwidth($properties->phone->value, 0, 25);\n\n return parsePhoneNumber($countryCode, $number);\n }\n\n return [];\n }\n\n /**\n * @param $properties\n */\n private function buildContactMobilePhone(?string $countryCode, $properties): ?string\n {\n return isset($properties['mobilephone'])\n ? Str::limit(phone_e164($countryCode, $properties['mobilephone']), 25, '')\n : null;\n }\n\n /**\n * @param $properties\n * @param $account\n */\n private function buildContactCountry($properties): ?string\n {\n if (is_array($properties) && empty($properties['country']) === false) {\n return $this->convertCountryNameToCode($properties['country']);\n }\n\n if (isset($properties->country)) {\n return $this->convertCountryNameToCode($properties->country->value);\n }\n\n return null;\n }\n\n /**\n * HubSpot doesn't have leads, so this method does nothing.\n *\n * @param Carbon $since\n * @param Carbon|null $to\n * @param string|null $crmProfileId\n *\n * @return int\n */\n public function syncLeads(Carbon $since, ?Carbon $to = null, ?string $crmProfileId = null): int\n {\n // Mark unused parameters to avoid code smell warnings\n unset($since, $to, $crmProfileId);\n\n return 0;\n }\n\n /**\n * HubSpot doesn't have leads.\n *\n * @param string $crmId\n *\n * @inheritdoc\n */\n public function syncLead(string $crmId): ?Lead\n {\n // Mark unused parameter to avoid code smell warnings\n unset($crmId);\n\n return null;\n }\n\n /**\n * Sync accounts (companies) modified since a given date (manual sync mode).\n *\n * This method fetches companies from HubSpot API based on modification date and\n * imports them one by one. It is used for:\n * - Manual sync commands (e.g., crm:sync-account with --from parameter)\n * - Initial sync for new teams\n * - Backfill operations\n *\n * For regular sync webhook batchSyncCompanies is used:\n *\n * @param Carbon $since Fetch companies modified after this date\n * @param Carbon|null $to Optional end date for modification range\n *\n * @return int Number of companies successfully synced\n */\n public function syncAccounts(Carbon $since, ?Carbon $to = null): int\n {\n $syncCount = 0;\n\n try {\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $fields = $this->getCompanyFields();\n $payload = $this->payloadBuilder->getRecentlyUpdatedSearchPayload($since, $to, $fields);\n\n foreach ($this->client->getPaginatedDataGenerator($payload, 'companies') as $hsAccount) {\n $this->importAccount($hsAccount);\n $syncCount++;\n }\n } catch (Exception $exception) {\n $this->logger->error('[' . $this->getDisplayName() . '] Sync accounts failed', [\n 'teamId' => $this->team->getUuid(),\n 'reason' => $exception->getMessage(),\n ]);\n }\n\n return $syncCount;\n }\n\n /**\n * @inheritdoc\n */\n public function syncAccount(string $crmId): ?Account\n {\n try {\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $fields = $this->getCompanyFields();\n $hsAccount = $this->client->getAccountById($crmId, $fields);\n } catch (\\HubSpot\\Client\\Crm\\Companies\\ApiException $e) {\n $this->logger->info('[' . $this->getDisplayName() . '] Account fetch failed', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n return null;\n } catch (CrmException $e) {\n $this->logger->info('[' . $this->getDisplayName() . '] Account not found', [\n 'teamId' => $this->team->getUuid(),\n 'crmId' => $crmId,\n 'reason' => $e->getMessage(),\n ]);\n\n return null;\n }\n\n return $this->importAccount($hsAccount);\n }\n\n /**\n * Process webhook-collected contact batches.\n *\n * Drains Redis sets containing contact CRM IDs collected from webhook events\n * and dispatches ImportContactBatch jobs for batch processing.\n *\n * @return int Number of contact IDs dispatched to jobs\n */\n public function batchSyncContacts(): int\n {\n $configId = $this->team->getCrmConfiguration()->getId();\n\n return $this->batchProcessor->processBatchesForObjectType(\n WebhookSyncBatchProcessor::OBJECT_TYPE_CONTACT,\n $configId\n );\n }\n\n public function importContactBatchByIds(array $crmIds): array\n {\n $result = [\n 'success_count' => 0,\n 'failed_ids' => [],\n 'errors' => [],\n ];\n\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $batchStart = microtime(true);\n $slowContacts = [];\n\n $fetchStart = microtime(true);\n $allContacts = $this->fetchContactsByIdsInChunks($crmIds);\n $fetchMs = (int) round((microtime(true) - $fetchStart) * 1000);\n\n $fetchedIds = array_map('strval', array_column($allContacts, 'id'));\n $notFoundIds = array_values(array_diff(array_map('strval', $crmIds), $fetchedIds));\n\n if (! empty($notFoundIds)) {\n $this->logger->info('[' . $this->getDisplayName() . '] Contact CRM IDs not found in HubSpot', [\n 'teamId' => $this->team->getId(),\n 'notFoundCount' => \\count($notFoundIds),\n 'notFoundIds' => $notFoundIds,\n 'requestedCount' => \\count($crmIds),\n 'fetchedCount' => \\count($allContacts),\n ]);\n }\n\n if (empty($allContacts)) {\n return $result;\n }\n\n $prepareStart = microtime(true);\n $accountMappings = $this->prepareAccountMappingsForContacts($allContacts);\n $prepareAccountsMs = (int) round((microtime(true) - $prepareStart) * 1000);\n\n $loopStart = microtime(true);\n foreach ($allContacts as $contactData) {\n $contactStart = microtime(true);\n\n try {\n $contact = $this->importContact($contactData, $accountMappings);\n if ($contact !== null) {\n $result['success_count']++;\n }\n } catch (\\Throwable $e) {\n $crmId = $contactData['id'] ?? 'unknown';\n $result['failed_ids'][] = $crmId;\n $result['errors'][$crmId] = $e->getMessage();\n\n $this->logger->warning('[' . $this->getDisplayName() . '] Failed to import contact', [\n 'teamId' => $this->team->getId(),\n 'crmId' => $crmId,\n 'error' => $e->getMessage(),\n ]);\n }\n\n $contactMs = (int) round((microtime(true) - $contactStart) * 1000);\n if ($contactMs > 1000) {\n $slowContacts[] = ['crmId' => $contactData['id'] ?? 'unknown', 'ms' => $contactMs];\n }\n }\n $loopMs = (int) round((microtime(true) - $loopStart) * 1000);\n $totalMs = (int) round((microtime(true) - $batchStart) * 1000);\n\n $this->logger->info('[' . $this->getDisplayName() . '] importContactBatch timing', [\n 'teamId' => $this->team->getId(),\n 'contact_count' => \\count($allContacts),\n 'requested_count' => \\count($crmIds),\n 'not_found_count' => \\count($notFoundIds),\n 'total_ms' => $totalMs,\n 'fetch_api_ms' => $fetchMs,\n 'prepare_accounts_ms' => $prepareAccountsMs,\n 'contacts_loop_ms' => $loopMs,\n 'avg_contact_ms' => ! empty($allContacts) ? (int) round($loopMs / \\count($allContacts)) : 0,\n 'slow_contacts_count' => \\count($slowContacts),\n 'slow_contacts' => array_slice($slowContacts, 0, 10),\n ]);\n\n return $result;\n }\n\n private function fetchContactsByIdsInChunks(array $crmIds): array\n {\n $fields = $this->getContactFields();\n $allContacts = [];\n\n foreach (array_chunk($crmIds, self::BATCH_SIZE) as $chunk) {\n try {\n $contacts = $this->client->getContactsByIds($chunk, $fields);\n foreach ($contacts as $contactData) {\n $allContacts[] = $contactData;\n }\n } catch (\\Throwable $e) {\n // @TODO what will happen if this exception is thrown\n $this->logger->warning('[' . $this->getDisplayName() . '] Batch contact fetch failed', [\n 'chunk_size' => \\count($chunk),\n 'error' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n }\n\n return $allContacts;\n }\n\n private function prepareAccountMappingsForContacts(array $contacts): array\n {\n $companyIds = [];\n foreach ($contacts as $contact) {\n $companyId = $contact['properties']['associatedcompanyid'] ?? null;\n if ($companyId !== null && $companyId !== '') {\n $companyIds[] = (string) $companyId;\n }\n }\n\n $companyIds = array_unique($companyIds);\n\n if (empty($companyIds)) {\n return [];\n }\n\n $mappings = $this->crmEntityRepository\n ->getExistingAccountIdsMap($this->config, $companyIds);\n\n $missingCompanyIds = array_diff($companyIds, array_keys($mappings));\n\n if (empty($missingCompanyIds)) {\n return $mappings;\n }\n\n $this->logger->info('[' . $this->getDisplayName() . '] Batch syncing missing accounts for contacts', [\n 'teamId' => $this->team->getId(),\n 'total_companies' => \\count($companyIds),\n 'existing_companies' => \\count($mappings),\n 'missing_companies' => \\count($missingCompanyIds),\n ]);\n\n try {\n $syncedAccounts = $this->batchSyncAccountsForContacts($missingCompanyIds);\n $mappings = array_merge($mappings, $syncedAccounts);\n } catch (\\Throwable $e) {\n $this->logger->error('[' . $this->getDisplayName() . '] Failed to batch sync missing accounts', [\n 'teamId' => $this->team->getId(),\n 'missingCompanyIds' => $missingCompanyIds,\n 'missingCount' => count($missingCompanyIds),\n 'error' => $e->getMessage(),\n ]);\n }\n\n return $mappings;\n }\n\n private function batchSyncAccountsForContacts(array $companyIds): array\n {\n $syncedAccounts = [];\n $fields = $this->getCompanyFields();\n\n foreach (array_chunk($companyIds, self::BATCH_SIZE) as $chunk) {\n try {\n $companies = $this->client->getCompaniesByIds($chunk, $fields);\n\n foreach ($companies as $companyData) {\n try {\n $account = $this->importAccount($companyData);\n if ($account) {\n $syncedAccounts[$account->getCrmProviderId()] = $account->getId();\n }\n } catch (\\Throwable $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Failed to import account in batch', [\n 'teamId' => $this->team->getId(),\n 'companyId' => $companyData['id'] ?? 'unknown',\n 'error' => $e->getMessage(),\n ]);\n }\n }\n } catch (\\Throwable $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Batch company fetch failed', [\n 'teamId' => $this->team->getId(),\n 'chunk_size' => \\count($chunk),\n 'error' => $e->getMessage(),\n ]);\n }\n }\n\n return $syncedAccounts;\n }\n\n /**\n * Process webhook-collected company batches.\n *\n * Drains Redis sets containing company CRM IDs collected from webhook events\n * and dispatches ImportAccountBatch jobs for batch processing.\n *\n * @return int Number of company IDs dispatched to jobs\n */\n public function batchSyncCompanies(): int\n {\n $configId = $this->team->getCrmConfiguration()->getId();\n\n return $this->batchProcessor->processBatchesForObjectType(\n WebhookSyncBatchProcessor::OBJECT_TYPE_COMPANY,\n $configId\n );\n }\n\n public function importAccountBatchByIds(array $crmIds): array\n {\n $result = [\n 'success_count' => 0,\n 'failed_ids' => [],\n 'errors' => [],\n ];\n\n if (! $this->client instanceof HubspotClientInterface) {\n throw new \\InvalidArgumentException('Client must implement HubspotClientInterface');\n }\n\n $batchStart = microtime(true);\n $slowAccounts = [];\n\n $fields = $this->getCompanyFields();\n $allCompanies = [];\n\n $fetchStart = microtime(true);\n foreach (array_chunk($crmIds, self::BATCH_SIZE) as $chunk) {\n try {\n $companies = $this->client->getCompaniesByIds($chunk, $fields);\n foreach ($companies as $companyData) {\n $allCompanies[] = $companyData;\n }\n } catch (\\Throwable $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Batch company fetch failed', [\n 'chunk_size' => \\count($chunk),\n 'error' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n }\n $fetchMs = (int) round((microtime(true) - $fetchStart) * 1000);\n\n $fetchedIds = array_map('strval', array_column($allCompanies, 'id'));\n $notFoundIds = array_values(array_diff(array_map('strval', $crmIds), $fetchedIds));\n\n if (! empty($notFoundIds)) {\n $this->logger->info('[' . $this->getDisplayName() . '] Company CRM IDs not found in HubSpot', [\n 'teamId' => $this->team->getId(),\n 'notFoundCount' => \\count($notFoundIds),\n 'notFoundIds' => $notFoundIds,\n 'requestedCount' => \\count($crmIds),\n 'fetchedCount' => \\count($allCompanies),\n ]);\n }\n\n $loopStart = microtime(true);\n foreach ($allCompanies as $companyData) {\n $accountStart = microtime(true);\n\n try {\n $account = $this->importAccount($companyData);\n if ($account !== null) {\n $result['success_count']++;\n }\n } catch (\\Throwable $e) {\n $crmId = $companyData['id'] ?? $companyData['properties']['hs_object_id'] ?? 'unknown';\n $result['failed_ids'][] = $crmId;\n $result['errors'][$crmId] = $e->getMessage();\n\n $this->logger->warning('[' . $this->getDisplayName() . '] Failed to import account', [\n 'crmId' => $crmId,\n 'error' => $e->getMessage(),\n ]);\n }\n\n $accountMs = (int) round((microtime(true) - $accountStart) * 1000);\n if ($accountMs > 1000) {\n $crmId = $companyData['id'] ?? $companyData['properties']['hs_object_id'] ?? 'unknown';\n $slowAccounts[] = ['crmId' => $crmId, 'ms' => $accountMs];\n }\n }\n $loopMs = (int) round((microtime(true) - $loopStart) * 1000);\n $totalMs = (int) round((microtime(true) - $batchStart) * 1000);\n\n $this->logger->info('[' . $this->getDisplayName() . '] importAccountBatch timing', [\n 'teamId' => $this->team->getId(),\n 'account_count' => \\count($allCompanies),\n 'requested_count' => \\count($crmIds),\n 'not_found_count' => \\count($notFoundIds),\n 'total_ms' => $totalMs,\n 'fetch_api_ms' => $fetchMs,\n 'accounts_loop_ms' => $loopMs,\n 'avg_account_ms' => ! empty($allCompanies) ? (int) round($loopMs / \\count($allCompanies)) : 0,\n 'slow_accounts_count' => \\count($slowAccounts),\n 'slow_accounts' => array_slice($slowAccounts, 0, 10),\n ]);\n\n return $result;\n }\n\n private function getCompanyFields(): array\n {\n return [\n 'country',\n 'name',\n 'phone',\n 'domain',\n 'hs_avatar_filemanager_key',\n 'industry',\n 'hubspot_owner_id',\n 'hs_object_id',\n 'createdate',\n 'hs_lastmodifieddate',\n ];\n }\n\n private function importAccount($crmData): ?Account\n {\n $crmProviderId = $crmData['id'] ?? $crmData['properties']['hs_object_id'] ?? null;\n\n $this->logger->info('[HubSpot] importAccount', [\n 'crm_provider_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n if (empty($crmData['properties'])) {\n $this->logger->info('[HubSpot] importAccount failed: empty properties', [\n 'crm_provider_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n return null;\n }\n\n $properties = $crmData['properties'];\n $crmId = (string) $properties['hs_object_id'];\n\n $countryCode = isset($properties['country']) ? $this->convertCountryNameToCode($properties['country']) : null;\n\n if (isset($properties['phone'])) {\n // Trim to our width and attempt to parse it.\n $number = mb_strimwidth($properties['phone'], 0, 25);\n $parsedNumber = parsePhoneNumber($countryCode, $number);\n } else {\n $parsedNumber = [];\n }\n\n $name = '[unknown]';\n if (isset($properties['name'])) {\n $name = $properties['name'];\n }\n\n $photoPath = $this->prospectPhotoPathService->getOrGeneratePhotoPath(\n $this->config,\n $crmId,\n Account::class,\n $crmId,\n $name\n );\n\n $industry = null;\n if (isset($properties['industry'])) {\n $industry = mb_strimwidth($properties['industry'], 0, 40);\n }\n\n $ownerId = $profile = null;\n if (isset($properties['hubspot_owner_id'])) {\n $ownerId = $properties['hubspot_owner_id'];\n $profile = $this->crmEntityRepository->findProfileByExternalId($this->config, (string) $ownerId);\n }\n\n $domain = null;\n if (isset($properties['domain'])) {\n $domain = StringUtil::resolveDomain($properties['domain']);\n }\n\n $remotelyCreatedAt = null;\n if (isset($properties['createdate']) && ! empty($properties['createdate'])) {\n $remotelyCreatedAt = Carbon::parse($properties['createdate']);\n }\n\n $data = [\n 'crm_provider_id' => $crmId,\n 'team_id' => $this->team->id,\n 'user_id' => $profile ? $profile->user_id : null,\n 'owner_id' => $ownerId,\n 'name' => mb_strimwidth($name, 0, 191),\n 'photo_path' => $photoPath,\n 'industry' => $industry,\n 'domain' => $domain !== null\n ? substr($domain, 0, 191)\n : null,\n 'phone' => $parsedNumber['phone'] ?? null,\n 'ext' => $parsedNumber['ext'] ?? null,\n 'country_code' => $countryCode,\n 'remotely_created_at' => $remotelyCreatedAt,\n ];\n\n return $this->crmEntityRepository->importAccount($this->config, $data);\n }\n\n public function deleteContact(string $crmProviderId): bool\n {\n try {\n $contact = $this->crmEntityRepository->findContactByExternalId($this->config, $crmProviderId);\n\n if (! $contact) {\n $this->logger->info('[HubSpot] Contact not found for deletion', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n // return success because we do not know which instance is the target\n return true;\n }\n\n $id = $contact->getId();\n\n $this->logger->info('[HubSpot] Deleting contact via webhook', [\n 'contact_id' => $id,\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n $contact->delete();\n DeleteContactJob::dispatch($id)->afterCommit();\n\n return true;\n } catch (Exception $e) {\n $this->logger->error('[HubSpot] Failed to delete contact via webhook', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n return false;\n }\n }\n\n public function deleteAccount(string $crmProviderId): bool\n {\n try {\n $account = $this->crmEntityRepository->findAccountByExternalId($this->config, $crmProviderId);\n\n if (! $account) {\n $this->logger->info('[HubSpot] Account not found for deletion', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n // return success because we do not know which instance is the target\n return true;\n }\n\n $id = $account->getId();\n\n $this->logger->info('[HubSpot] Deleting account via webhook', [\n 'account_id' => $id,\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n $account->delete();\n DeleteAccountJob::dispatch($id)->afterCommit();\n\n return true;\n } catch (Exception $e) {\n $this->logger->error('[HubSpot] Failed to delete account via webhook', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n return false;\n }\n }\n\n public function deleteOpportunity(string $crmProviderId): bool\n {\n try {\n $opportunity = $this->crmEntityRepository->findOpportunityByExternalId($this->config, $crmProviderId);\n\n if (! $opportunity) {\n $this->logger->info('[HubSpot] Opportunity not found for deletion', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n // return success because we do not know which instance is the target\n return true;\n }\n\n $id = $opportunity->getId();\n\n $this->logger->info('[HubSpot] Deleting opportunity via webhook', [\n 'opportunity_id' => $id,\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n ]);\n\n $opportunity->delete();\n DeleteOpportunityJob::dispatch($id)->afterCommit();\n\n return true;\n } catch (Exception $e) {\n $this->logger->error('[HubSpot] Failed to delete opportunity via webhook', [\n 'crm_provider_id' => $crmProviderId,\n 'team_id' => $this->team->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n return false;\n }\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-8046841189839988101
|
-4187333948484548378
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
Editor for custom.log
Sync Changes
Hide This Notification
Code changed:
Hide
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\ServiceTraits;
use Carbon\Carbon;
use Exception;
use Illuminate\Support\Str;
use Jiminny\Exceptions\CrmException;
use Jiminny\Jobs\Crm\Delete\DeleteAccountJob;
use Jiminny\Jobs\Crm\Delete\DeleteContactJob;
use Jiminny\Jobs\Crm\Delete\DeleteOpportunityJob;
use Jiminny\Models\Account;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Repositories\Crm\CrmEntityRepository;
use Jiminny\Services\Avatar\ProspectPhotoPathService;
use Jiminny\Services\Crm\Hubspot\HubspotClientInterface;
use Jiminny\Services\Crm\Hubspot\WebhookSyncBatchProcessor;
use Jiminny\Utils\StringUtil;
trait SyncCrmEntitiesTrait
{
use OpportunitySyncTrait;
private const string CDN_URL = '[URL_WITH_CREDENTIALS] Carbon $since Fetch contacts modified after this date
* @param Carbon|null $to Optional end date for modification range
*
* @return int Number of contacts successfully synced
*/
public function syncContacts(Carbon $since, ?Carbon $to = null): int
{
$syncCount = 0;
try {
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$fields = $this->getContactFields();
$payload = $this->payloadBuilder->getRecentlyUpdatedSearchPayload($since, $to, $fields);
foreach ($this->client->getPaginatedDataGenerator($payload, 'contacts') as $hsContact) {
$this->importContact($hsContact);
$syncCount++;
}
} catch (Exception $exception) {
$this->logger->error('[' . $this->getDisplayName() . '] Sync contacts failed', [
'teamId' => $this->team->getUuid(),
'reason' => $exception->getMessage(),
]);
}
return $syncCount;
}
/**
* @inheritdoc
*/
public function syncContact(string $crmId): ?Contact
{
try {
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$fields = $this->getContactFields();
$hsContact = $this->client->getContactById($crmId, $fields);
} catch (\HubSpot\Client\Crm\Contacts\ApiException $e) {
$this->logger->info('[' . $this->getDisplayName() . '] Contact fetch failed', [
'teamId' => $this->team->getUuid(),
'crmId' => $crmId,
'reason' => $e->getMessage(),
]);
return null;
} catch (CrmException $e) {
$this->logger->info('[' . $this->getDisplayName() . '] Contact not found', [
'teamId' => $this->team->getUuid(),
'crmId' => $crmId,
'reason' => $e->getMessage(),
]);
return null;
}
if (empty($hsContact['properties']) || empty($hsContact['id'])) {
$this->logger->warning('[' . $this->getDisplayName() . '] Contact data incomplete', [
'teamId' => $this->team->getUuid(),
'crmId' => $crmId,
'has_properties' => ! empty($hsContact['properties']),
'has_id' => ! empty($hsContact['id']),
]);
return null;
}
return $this->importContact($hsContact);
}
private function getContactFields(): array
{
return [
'associatedcompanyid',
'country',
'firstname',
'lastname',
'phone',
'mobilephone',
'email',
'photo',
'hs_avatar_filemanager_key',
'jobtitle',
'hubspot_owner_id',
'hs_object_id',
'createdate',
'hs_lastmodifieddate',
];
}
/**
* @inheritdoc
*/
private function importContact($crmData, array $accountMappings = []): ?Contact
{
$crmProviderId = $crmData['id'] ?? null;
$this->logger->info('[HubSpot] importContact', [
'crm_provider_id' => $crmProviderId,
'config_id' => $this->config->getId(),
]);
if (empty($crmData['properties'])) {
$this->logger->info('[HubSpot] importContact failed: empty properties', [
'crm_provider_id' => $crmProviderId,
'config_id' => $this->config->getId(),
]);
return null;
}
$properties = $crmData['properties'];
$crmId = (string) $crmData['id'];
$accountId = $this->resolveContactAccount($properties, $accountMappings);
$data = $this->buildContactData($crmId, $properties, $accountId);
return $this->crmEntityRepository->importContact($this->config, $data);
}
private function resolveContactAccount(array $properties, array $accountMappings): ?int
{
if (empty($properties['associatedcompanyid'])) {
return null;
}
$companyId = (string) $properties['associatedcompanyid'];
if (! empty($accountMappings)) {
return $accountMappings[$companyId] ?? null;
}
return $this->crmEntityRepository->findAccountByExternalId(
$this->team->getCrmConfiguration(),
$companyId
)?->getId() ?? $this->syncAccount($companyId)?->getId();
}
private function buildContactData(string $crmId, array $properties, ?int $accountId): array
{
$countryCode = $this->buildContactCountry($properties);
$name = $this->buildContactName($properties);
$photoPath = $this->teamService->generateAvatar(
$crmId,
empty($name) ? ($properties['email'] ?? 'N/A') : $name,
);
$parsedNumber = $this->buildContactPhone($countryCode, $properties);
$mobileNumber = $this->buildContactMobilePhone($countryCode, $properties);
$ownerId = $properties['hubspot_owner_id'] ?? null;
$profile = $ownerId !== null
? $this->crmEntityRepository->findProfileByExternalId($this->config, (string) $ownerId)
: null;
$ext = (isset($parsedNumber['ext']) && is_string($parsedNumber['ext']) && strlen($parsedNumber['ext']) <= 10)
? $parsedNumber['ext']
: null;
$title = isset($properties['jobtitle']) ? mb_strimwidth($properties['jobtitle'], 0, 128) : null;
$email = isset($properties['email']) ? mb_strimwidth($properties['email'], 0, 191) : null;
$remotelyCreatedAt = ! empty($properties['createdate']) ? Carbon::parse($properties['createdate']) : null;
return [
'crm_provider_id' => $crmId,
'team_id' => $this->team->getId(),
'account_id' => $accountId,
'user_id' => $profile?->getUserId(),
'owner_id' => $ownerId,
'name' => $name,
'title' => $title,
'email' => $email,
'country_code' => $countryCode,
'phone' => $parsedNumber['phone'] ?? null,
'mobile_phone' => $mobileNumber ?? null,
'ext' => $ext,
'photo_path' => $photoPath,
'remotely_created_at' => $remotelyCreatedAt,
];
}
/**
* @param $properties
*/
private function buildContactName($properties): string
{
if (is_array($properties)) {
return $this->buildContactNameFromArray($properties);
}
return $this->buildContactNameFromObject($properties);
}
private function buildContactNameFromArray(array $properties): string
{
if (! empty($properties['name'])) {
return mb_strimwidth($properties['name'], 0, 100);
}
$name = '';
if (! empty($properties['firstname'])) {
$name = $properties['firstname'] . ' ';
}
if (! empty($properties['lastname'])) {
$name .= $properties['lastname'];
}
if ($name === '' && ! empty($properties['email'])) {
$name = $properties['email'];
}
return mb_strimwidth($name, 0, 100);
}
private function buildContactNameFromObject($properties): string
{
$name = '';
if (isset($properties->firstname)) {
$name = $properties->firstname->value . ' ';
}
if (isset($properties->lastname)) {
$name .= $properties->lastname->value;
}
if ($name === '' && isset($properties->email)) {
$name = $properties->email->value;
}
return mb_strimwidth($name, 0, 100);
}
/**
* @param $properties
*/
private function buildContactPhone(?string $countryCode, $properties): ?array
{
if (is_array($properties) && empty($properties['phone']) === false) {
$number = mb_strimwidth($properties['phone'], 0, 25);
return parsePhoneNumber($countryCode, $number);
} elseif (isset($properties->phone)) {
$number = mb_strimwidth($properties->phone->value, 0, 25);
return parsePhoneNumber($countryCode, $number);
}
return [];
}
/**
* @param $properties
*/
private function buildContactMobilePhone(?string $countryCode, $properties): ?string
{
return isset($properties['mobilephone'])
? Str::limit(phone_e164($countryCode, $properties['mobilephone']), 25, '')
: null;
}
/**
* @param $properties
* @param $account
*/
private function buildContactCountry($properties): ?string
{
if (is_array($properties) && empty($properties['country']) === false) {
return $this->convertCountryNameToCode($properties['country']);
}
if (isset($properties->country)) {
return $this->convertCountryNameToCode($properties->country->value);
}
return null;
}
/**
* HubSpot doesn't have leads, so this method does nothing.
*
* @param Carbon $since
* @param Carbon|null $to
* @param string|null $crmProfileId
*
* @return int
*/
public function syncLeads(Carbon $since, ?Carbon $to = null, ?string $crmProfileId = null): int
{
// Mark unused parameters to avoid code smell warnings
unset($since, $to, $crmProfileId);
return 0;
}
/**
* HubSpot doesn't have leads.
*
* @param string $crmId
*
* @inheritdoc
*/
public function syncLead(string $crmId): ?Lead
{
// Mark unused parameter to avoid code smell warnings
unset($crmId);
return null;
}
/**
* Sync accounts (companies) modified since a given date (manual sync mode).
*
* This method fetches companies from HubSpot API based on modification date and
* imports them one by one. It is used for:
* - Manual sync commands (e.g., crm:sync-account with --from parameter)
* - Initial sync for new teams
* - Backfill operations
*
* For regular sync webhook batchSyncCompanies is used:
*
* @param Carbon $since Fetch companies modified after this date
* @param Carbon|null $to Optional end date for modification range
*
* @return int Number of companies successfully synced
*/
public function syncAccounts(Carbon $since, ?Carbon $to = null): int
{
$syncCount = 0;
try {
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$fields = $this->getCompanyFields();
$payload = $this->payloadBuilder->getRecentlyUpdatedSearchPayload($since, $to, $fields);
foreach ($this->client->getPaginatedDataGenerator($payload, 'companies') as $hsAccount) {
$this->importAccount($hsAccount);
$syncCount++;
}
} catch (Exception $exception) {
$this->logger->error('[' . $this->getDisplayName() . '] Sync accounts failed', [
'teamId' => $this->team->getUuid(),
'reason' => $exception->getMessage(),
]);
}
return $syncCount;
}
/**
* @inheritdoc
*/
public function syncAccount(string $crmId): ?Account
{
try {
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$fields = $this->getCompanyFields();
$hsAccount = $this->client->getAccountById($crmId, $fields);
} catch (\HubSpot\Client\Crm\Companies\ApiException $e) {
$this->logger->info('[' . $this->getDisplayName() . '] Account fetch failed', [
'teamId' => $this->team->getUuid(),
'crmId' => $crmId,
'reason' => $e->getMessage(),
]);
return null;
} catch (CrmException $e) {
$this->logger->info('[' . $this->getDisplayName() . '] Account not found', [
'teamId' => $this->team->getUuid(),
'crmId' => $crmId,
'reason' => $e->getMessage(),
]);
return null;
}
return $this->importAccount($hsAccount);
}
/**
* Process webhook-collected contact batches.
*
* Drains Redis sets containing contact CRM IDs collected from webhook events
* and dispatches ImportContactBatch jobs for batch processing.
*
* @return int Number of contact IDs dispatched to jobs
*/
public function batchSyncContacts(): int
{
$configId = $this->team->getCrmConfiguration()->getId();
return $this->batchProcessor->processBatchesForObjectType(
WebhookSyncBatchProcessor::OBJECT_TYPE_CONTACT,
$configId
);
}
public function importContactBatchByIds(array $crmIds): array
{
$result = [
'success_count' => 0,
'failed_ids' => [],
'errors' => [],
];
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$batchStart = microtime(true);
$slowContacts = [];
$fetchStart = microtime(true);
$allContacts = $this->fetchContactsByIdsInChunks($crmIds);
$fetchMs = (int) round((microtime(true) - $fetchStart) * 1000);
$fetchedIds = array_map('strval', array_column($allContacts, 'id'));
$notFoundIds = array_values(array_diff(array_map('strval', $crmIds), $fetchedIds));
if (! empty($notFoundIds)) {
$this->logger->info('[' . $this->getDisplayName() . '] Contact CRM IDs not found in HubSpot', [
'teamId' => $this->team->getId(),
'notFoundCount' => \count($notFoundIds),
'notFoundIds' => $notFoundIds,
'requestedCount' => \count($crmIds),
'fetchedCount' => \count($allContacts),
]);
}
if (empty($allContacts)) {
return $result;
}
$prepareStart = microtime(true);
$accountMappings = $this->prepareAccountMappingsForContacts($allContacts);
$prepareAccountsMs = (int) round((microtime(true) - $prepareStart) * 1000);
$loopStart = microtime(true);
foreach ($allContacts as $contactData) {
$contactStart = microtime(true);
try {
$contact = $this->importContact($contactData, $accountMappings);
if ($contact !== null) {
$result['success_count']++;
}
} catch (\Throwable $e) {
$crmId = $contactData['id'] ?? 'unknown';
$result['failed_ids'][] = $crmId;
$result['errors'][$crmId] = $e->getMessage();
$this->logger->warning('[' . $this->getDisplayName() . '] Failed to import contact', [
'teamId' => $this->team->getId(),
'crmId' => $crmId,
'error' => $e->getMessage(),
]);
}
$contactMs = (int) round((microtime(true) - $contactStart) * 1000);
if ($contactMs > 1000) {
$slowContacts[] = ['crmId' => $contactData['id'] ?? 'unknown', 'ms' => $contactMs];
}
}
$loopMs = (int) round((microtime(true) - $loopStart) * 1000);
$totalMs = (int) round((microtime(true) - $batchStart) * 1000);
$this->logger->info('[' . $this->getDisplayName() . '] importContactBatch timing', [
'teamId' => $this->team->getId(),
'contact_count' => \count($allContacts),
'requested_count' => \count($crmIds),
'not_found_count' => \count($notFoundIds),
'total_ms' => $totalMs,
'fetch_api_ms' => $fetchMs,
'prepare_accounts_ms' => $prepareAccountsMs,
'contacts_loop_ms' => $loopMs,
'avg_contact_ms' => ! empty($allContacts) ? (int) round($loopMs / \count($allContacts)) : 0,
'slow_contacts_count' => \count($slowContacts),
'slow_contacts' => array_slice($slowContacts, 0, 10),
]);
return $result;
}
private function fetchContactsByIdsInChunks(array $crmIds): array
{
$fields = $this->getContactFields();
$allContacts = [];
foreach (array_chunk($crmIds, self::BATCH_SIZE) as $chunk) {
try {
$contacts = $this->client->getContactsByIds($chunk, $fields);
foreach ($contacts as $contactData) {
$allContacts[] = $contactData;
}
} catch (\Throwable $e) {
// @TODO what will happen if this exception is thrown
$this->logger->warning('[' . $this->getDisplayName() . '] Batch contact fetch failed', [
'chunk_size' => \count($chunk),
'error' => $e->getMessage(),
]);
throw $e;
}
}
return $allContacts;
}
private function prepareAccountMappingsForContacts(array $contacts): array
{
$companyIds = [];
foreach ($contacts as $contact) {
$companyId = $contact['properties']['associatedcompanyid'] ?? null;
if ($companyId !== null && $companyId !== '') {
$companyIds[] = (string) $companyId;
}
}
$companyIds = array_unique($companyIds);
if (empty($companyIds)) {
return [];
}
$mappings = $this->crmEntityRepository
->getExistingAccountIdsMap($this->config, $companyIds);
$missingCompanyIds = array_diff($companyIds, array_keys($mappings));
if (empty($missingCompanyIds)) {
return $mappings;
}
$this->logger->info('[' . $this->getDisplayName() . '] Batch syncing missing accounts for contacts', [
'teamId' => $this->team->getId(),
'total_companies' => \count($companyIds),
'existing_companies' => \count($mappings),
'missing_companies' => \count($missingCompanyIds),
]);
try {
$syncedAccounts = $this->batchSyncAccountsForContacts($missingCompanyIds);
$mappings = array_merge($mappings, $syncedAccounts);
} catch (\Throwable $e) {
$this->logger->error('[' . $this->getDisplayName() . '] Failed to batch sync missing accounts', [
'teamId' => $this->team->getId(),
'missingCompanyIds' => $missingCompanyIds,
'missingCount' => count($missingCompanyIds),
'error' => $e->getMessage(),
]);
}
return $mappings;
}
private function batchSyncAccountsForContacts(array $companyIds): array
{
$syncedAccounts = [];
$fields = $this->getCompanyFields();
foreach (array_chunk($companyIds, self::BATCH_SIZE) as $chunk) {
try {
$companies = $this->client->getCompaniesByIds($chunk, $fields);
foreach ($companies as $companyData) {
try {
$account = $this->importAccount($companyData);
if ($account) {
$syncedAccounts[$account->getCrmProviderId()] = $account->getId();
}
} catch (\Throwable $e) {
$this->logger->warning('[' . $this->getDisplayName() . '] Failed to import account in batch', [
'teamId' => $this->team->getId(),
'companyId' => $companyData['id'] ?? 'unknown',
'error' => $e->getMessage(),
]);
}
}
} catch (\Throwable $e) {
$this->logger->warning('[' . $this->getDisplayName() . '] Batch company fetch failed', [
'teamId' => $this->team->getId(),
'chunk_size' => \count($chunk),
'error' => $e->getMessage(),
]);
}
}
return $syncedAccounts;
}
/**
* Process webhook-collected company batches.
*
* Drains Redis sets containing company CRM IDs collected from webhook events
* and dispatches ImportAccountBatch jobs for batch processing.
*
* @return int Number of company IDs dispatched to jobs
*/
public function batchSyncCompanies(): int
{
$configId = $this->team->getCrmConfiguration()->getId();
return $this->batchProcessor->processBatchesForObjectType(
WebhookSyncBatchProcessor::OBJECT_TYPE_COMPANY,
$configId
);
}
public function importAccountBatchByIds(array $crmIds): array
{
$result = [
'success_count' => 0,
'failed_ids' => [],
'errors' => [],
];
if (! $this->client instanceof HubspotClientInterface) {
throw new \InvalidArgumentException('Client must implement HubspotClientInterface');
}
$batchStart = microtime(true);
$slowAccounts = [];
$fields = $this->getCompanyFields();
$allCompanies = [];
$fetchStart = microtime(true);
foreach (array_chunk($crmIds, self::BATCH_SIZE) as $chunk) {
try {
$companies = $this->client->getCompaniesByIds($chunk, $fields);
foreach ($companies as $companyData) {
$allCompanies[] = $companyData;
}
} catch (\Throwable $e) {
$this->logger->warning('[' . $this->getDisplayName() . '] Batch company fetch failed', [
'chunk_size' => \count($chunk),
'error' => $e->getMessage(),
]);
throw $e;
}
}
$fetchMs = (int) round((microtime(true) - $fetchStart) * 1000);
$fetchedIds = array_map('strval', array_column($allCompanies, 'id'));
$notFoundIds = array_values(array_diff(array_map('strval', $crmIds), $fetchedIds));
if (! empty($notFoundIds)) {
$this->logger->info('[' . $this->getDisplayName() . '] Company CRM IDs not found in HubSpot', [
'teamId' => $this->team->getId(),
'notFoundCount' => \count($notFoundIds),
'notFoundIds' => $notFoundIds,
'requestedCount' => \count($crmIds),
'fetchedCount' => \count($allCompanies),
]);
}
$loopStart = microtime(true);
foreach ($allCompanies as $companyData) {
$accountStart = microtime(true);
try {
$account = $this->importAccount($companyData);
if ($account !== null) {
$result['success_count']++;
}
} catch (\Throwable $e) {
$crmId = $companyData['id'] ?? $companyData['properties']['hs_object_id'] ?? 'unknown';
$result['failed_ids'][] = $crmId;
$result['errors'][$crmId] = $e->getMessage();
$this->logger->warning('[' . $this->getDisplayName() . '] Failed to import account', [
'crmId' => $crmId,
'error' => $e->getMessage(),
]);
}
$accountMs = (int) round((microtime(true) - $accountStart) * 1000);
if ($accountMs > 1000) {
$crmId = $companyData['id'] ?? $companyData['properties']['hs_object_id'] ?? 'unknown';
$slowAccounts[] = ['crmId' => $crmId, 'ms' => $accountMs];
}
}
$loopMs = (int) round((microtime(true) - $loopStart) * 1000);
$totalMs = (int) round((microtime(true) - $batchStart) * 1000);
$this->logger->info('[' . $this->getDisplayName() . '] importAccountBatch timing', [
'teamId' => $this->team->getId(),
'account_count' => \count($allCompanies),
'requested_count' => \count($crmIds),
'not_found_count' => \count($notFoundIds),
'total_ms' => $totalMs,
'fetch_api_ms' => $fetchMs,
'accounts_loop_ms' => $loopMs,
'avg_account_ms' => ! empty($allCompanies) ? (int) round($loopMs / \count($allCompanies)) : 0,
'slow_accounts_count' => \count($slowAccounts),
'slow_accounts' => array_slice($slowAccounts, 0, 10),
]);
return $result;
}
private function getCompanyFields(): array
{
return [
'country',
'name',
'phone',
'domain',
'hs_avatar_filemanager_key',
'industry',
'hubspot_owner_id',
'hs_object_id',
'createdate',
'hs_lastmodifieddate',
];
}
private function importAccount($crmData): ?Account
{
$crmProviderId = $crmData['id'] ?? $crmData['properties']['hs_object_id'] ?? null;
$this->logger->info('[HubSpot] importAccount', [
'crm_provider_id' => $crmProviderId,
'config_id' => $this->config->getId(),
]);
if (empty($crmData['properties'])) {
$this->logger->info('[HubSpot] importAccount failed: empty properties', [
'crm_provider_id' => $crmProviderId,
'config_id' => $this->config->getId(),
]);
return null;
}
$properties = $crmData['properties'];
$crmId = (string) $properties['hs_object_id'];
$countryCode = isset($properties['country']) ? $this->convertCountryNameToCode($properties['country']) : null;
if (isset($properties['phone'])) {
// Trim to our width and attempt to parse it.
$number = mb_strimwidth($properties['phone'], 0, 25);
$parsedNumber = parsePhoneNumber($countryCode, $number);
} else {
$parsedNumber = [];
}
$name = '[unknown]';
if (isset($properties['name'])) {
$name = $properties['name'];
}
$photoPath = $this->prospectPhotoPathService->getOrGeneratePhotoPath(
$this->config,
$crmId,
Account::class,
$crmId,
$name
);
$industry = null;
if (isset($properties['industry'])) {
$industry = mb_strimwidth($properties['industry'], 0, 40);
}
$ownerId = $profile = null;
if (isset($properties['hubspot_owner_id'])) {
$ownerId = $properties['hubspot_owner_id'];
$profile = $this->crmEntityRepository->findProfileByExternalId($this->config, (string) $ownerId);
}
$domain = null;
if (isset($properties['domain'])) {
$domain = StringUtil::resolveDomain($properties['domain']);
}
$remotelyCreatedAt = null;
if (isset($properties['createdate']) && ! empty($properties['createdate'])) {
$remotelyCreatedAt = Carbon::parse($properties['createdate']);
}
$data = [
'crm_provider_id' => $crmId,
'team_id' => $this->team->id,
'user_id' => $profile ? $profile->user_id : null,
'owner_id' => $ownerId,
'name' => mb_strimwidth($name, 0, 191),
'photo_path' => $photoPath,
'industry' => $industry,
'domain' => $domain !== null
? substr($domain, 0, 191)
: null,
'phone' => $parsedNumber['phone'] ?? null,
'ext' => $parsedNumber['ext'] ?? null,
'country_code' => $countryCode,
'remotely_created_at' => $remotelyCreatedAt,
];
return $this->crmEntityRepository->importAccount($this->config, $data);
}
public function deleteContact(string $crmProviderId): bool
{
try {
$contact = $this->crmEntityRepository->findContactByExternalId($this->config, $crmProviderId);
if (! $contact) {
$this->logger->info('[HubSpot] Contact not found for deletion', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
// return success because we do not know which instance is the target
return true;
}
$id = $contact->getId();
$this->logger->info('[HubSpot] Deleting contact via webhook', [
'contact_id' => $id,
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
$contact->delete();
DeleteContactJob::dispatch($id)->afterCommit();
return true;
} catch (Exception $e) {
$this->logger->error('[HubSpot] Failed to delete contact via webhook', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
'error' => $e->getMessage(),
]);
return false;
}
}
public function deleteAccount(string $crmProviderId): bool
{
try {
$account = $this->crmEntityRepository->findAccountByExternalId($this->config, $crmProviderId);
if (! $account) {
$this->logger->info('[HubSpot] Account not found for deletion', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
// return success because we do not know which instance is the target
return true;
}
$id = $account->getId();
$this->logger->info('[HubSpot] Deleting account via webhook', [
'account_id' => $id,
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
$account->delete();
DeleteAccountJob::dispatch($id)->afterCommit();
return true;
} catch (Exception $e) {
$this->logger->error('[HubSpot] Failed to delete account via webhook', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
'error' => $e->getMessage(),
]);
return false;
}
}
public function deleteOpportunity(string $crmProviderId): bool
{
try {
$opportunity = $this->crmEntityRepository->findOpportunityByExternalId($this->config, $crmProviderId);
if (! $opportunity) {
$this->logger->info('[HubSpot] Opportunity not found for deletion', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
// return success because we do not know which instance is the target
return true;
}
$id = $opportunity->getId();
$this->logger->info('[HubSpot] Deleting opportunity via webhook', [
'opportunity_id' => $id,
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
]);
$opportunity->delete();
DeleteOpportunityJob::dispatch($id)->afterCommit();
return true;
} catch (Exception $e) {
$this->logger->error('[HubSpot] Failed to delete opportunity via webhook', [
'crm_provider_id' => $crmProviderId,
'team_id' => $this->team->getId(),
'error' => $e->getMessage(),
]);
return false;
}
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
2837
|
NULL
|
NULL
|
NULL
|