|
17919
|
780
|
38
|
2026-05-11T10:41:35.129052+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778496095129_m1.jpg...
|
PhpStorm
|
faVsco.js – Client.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
3
68
3
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\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 Illuminate\Support\Facades\Redis;
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)
{
$cacheKey = $this->getRateLimitCacheKey();
$cachedRetryAfter = Redis::get($cacheKey);
if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {
throw new RateLimitException(
'Hubspot rate limit (cached circuit-breaker)',
(int) $cachedRetryAfter,
);
}
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'policy' => $this->parsePolicy($e),
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function getRateLimitCacheKey(): string
{
return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());
}
public function isHubspotRateLimit(Throwable $e): bool
{
if ($e instanceof BadRequest
|| $e instanceof DealApiException
|| $e instanceof ContactApiException
|| $e instanceof CompanyApiException
|| $e instanceof \GuzzleHttp\Exception\RequestException
) {
return (int) $e->getCode() === 429;
}
return false;
}
public function parseRetryAfter(Throwable $e): int
{
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$e ' . PHP_EOL . print_r($e, true));
return 5;
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;
}
}
$policy = $this->parsePolicy($e);
if ($policy === 'TEN_SECONDLY_ROLLING') {
return 10;
}
if ($policy === 'SECONDLY') {
return 1;
}
if ($policy === 'DAILY_LIMIT') {
return 600;
}
$this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [
'exception_class' => get_class($e),
]);
return 10;
}
public function parsePolicy(Throwable $e): ?string
{
if (! method_exists($e, 'getResponseBody')) {
return null;
}
$body = $e->getResponseBody();
if (is_string($body)) {
$body = json_decode($body, true) ?? [];
}
if (! is_array($body)) {
return null;
}
$policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;
return is_string($policy) ? strtoupper($policy) : null;
}
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
);
}
/**
* Execute a search request against HubSpot CRM objects with rate limiting.
*
* @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')
* @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.
* @return array The search response with 'results', 'total', 'paging' keys
* @throws RateLimitException When rate limit is hit
* @throws HubspotException On API errors
*/
public function search(string $objectType, array $payload): array
{
$endpoint = self::BASE_URL . "/crm/v3/objects/{$objectType}/search";
return $this->executeRequest(function () use ($endpoint, $payload) {
$response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
return $response->toArray();
});
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$deal = $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') {
return $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
return $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
}
/**
* @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 (RateLimitException $e) {
// throw $e;
} 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);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
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":"JY-20725-handle-HS-search-rate-limit, menu","depth":5,"on_screen":true,"help_text":"Git Branch: JY-20725-handle-HS-search-rate-limit","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":"HandleHubspotRateLimitTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'HandleHubspotRateLimitTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'HandleHubspotRateLimitTest'","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":"AXStaticText","text":"3","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"68","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"3","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"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\\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 Illuminate\\Support\\Facades\\Redis;\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\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Reacts to a rate limits (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 $cacheKey = $this->getRateLimitCacheKey();\n\n $cachedRetryAfter = Redis::get($cacheKey);\n if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n (int) $cachedRetryAfter,\n );\n }\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);\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 'policy' => $this->parsePolicy($e),\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 getRateLimitCacheKey(): string\n {\n return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());\n }\n\n public function isHubspotRateLimit(Throwable $e): bool\n {\n if ($e instanceof BadRequest\n || $e instanceof DealApiException\n || $e instanceof ContactApiException\n || $e instanceof CompanyApiException\n || $e instanceof \\GuzzleHttp\\Exception\\RequestException\n ) {\n return (int) $e->getCode() === 429;\n }\n\n return false;\n }\n\n public function parseRetryAfter(Throwable $e): int\n {\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$e ' . PHP_EOL . print_r($e, true));\n return 5;\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 $policy = $this->parsePolicy($e);\n if ($policy === 'TEN_SECONDLY_ROLLING') {\n return 10;\n }\n if ($policy === 'SECONDLY') {\n return 1;\n }\n if ($policy === 'DAILY_LIMIT') {\n return 600;\n }\n\n $this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [\n 'exception_class' => get_class($e),\n ]);\n\n return 10;\n }\n\n public function parsePolicy(Throwable $e): ?string\n {\n if (! method_exists($e, 'getResponseBody')) {\n return null;\n }\n\n $body = $e->getResponseBody();\n if (is_string($body)) {\n $body = json_decode($body, true) ?? [];\n }\n\n if (! is_array($body)) {\n return null;\n }\n\n $policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;\n\n return is_string($policy) ? strtoupper($policy) : null;\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 * Execute a search request against HubSpot CRM objects with rate limiting.\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n * @return array The search response with 'results', 'total', 'paging' keys\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n */\n public function search(string $objectType, array $payload): array\n {\n $endpoint = self::BASE_URL . \"/crm/v3/objects/{$objectType}/search\";\n\n return $this->executeRequest(function () use ($endpoint, $payload) {\n $response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n return $response->toArray();\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 $deal = $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 return $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n return $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\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 (RateLimitException $e) {\n// throw $e;\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\\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 Illuminate\\Support\\Facades\\Redis;\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\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Reacts to a rate limits (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 $cacheKey = $this->getRateLimitCacheKey();\n\n $cachedRetryAfter = Redis::get($cacheKey);\n if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n (int) $cachedRetryAfter,\n );\n }\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);\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 'policy' => $this->parsePolicy($e),\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 getRateLimitCacheKey(): string\n {\n return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());\n }\n\n public function isHubspotRateLimit(Throwable $e): bool\n {\n if ($e instanceof BadRequest\n || $e instanceof DealApiException\n || $e instanceof ContactApiException\n || $e instanceof CompanyApiException\n || $e instanceof \\GuzzleHttp\\Exception\\RequestException\n ) {\n return (int) $e->getCode() === 429;\n }\n\n return false;\n }\n\n public function parseRetryAfter(Throwable $e): int\n {\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$e ' . PHP_EOL . print_r($e, true));\n return 5;\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 $policy = $this->parsePolicy($e);\n if ($policy === 'TEN_SECONDLY_ROLLING') {\n return 10;\n }\n if ($policy === 'SECONDLY') {\n return 1;\n }\n if ($policy === 'DAILY_LIMIT') {\n return 600;\n }\n\n $this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [\n 'exception_class' => get_class($e),\n ]);\n\n return 10;\n }\n\n public function parsePolicy(Throwable $e): ?string\n {\n if (! method_exists($e, 'getResponseBody')) {\n return null;\n }\n\n $body = $e->getResponseBody();\n if (is_string($body)) {\n $body = json_decode($body, true) ?? [];\n }\n\n if (! is_array($body)) {\n return null;\n }\n\n $policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;\n\n return is_string($policy) ? strtoupper($policy) : null;\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 * Execute a search request against HubSpot CRM objects with rate limiting.\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n * @return array The search response with 'results', 'total', 'paging' keys\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n */\n public function search(string $objectType, array $payload): array\n {\n $endpoint = self::BASE_URL . \"/crm/v3/objects/{$objectType}/search\";\n\n return $this->executeRequest(function () use ($endpoint, $payload) {\n $response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n return $response->toArray();\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 $deal = $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 return $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n return $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\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 (RateLimitException $e) {\n// throw $e;\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":"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":"19","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","depth":4,"on_screen":true,"value":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","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}]...
|
-6243931637791501581
|
6378618611371477092
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
3
68
3
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\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 Illuminate\Support\Facades\Redis;
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)
{
$cacheKey = $this->getRateLimitCacheKey();
$cachedRetryAfter = Redis::get($cacheKey);
if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {
throw new RateLimitException(
'Hubspot rate limit (cached circuit-breaker)',
(int) $cachedRetryAfter,
);
}
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'policy' => $this->parsePolicy($e),
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function getRateLimitCacheKey(): string
{
return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());
}
public function isHubspotRateLimit(Throwable $e): bool
{
if ($e instanceof BadRequest
|| $e instanceof DealApiException
|| $e instanceof ContactApiException
|| $e instanceof CompanyApiException
|| $e instanceof \GuzzleHttp\Exception\RequestException
) {
return (int) $e->getCode() === 429;
}
return false;
}
public function parseRetryAfter(Throwable $e): int
{
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$e ' . PHP_EOL . print_r($e, true));
return 5;
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;
}
}
$policy = $this->parsePolicy($e);
if ($policy === 'TEN_SECONDLY_ROLLING') {
return 10;
}
if ($policy === 'SECONDLY') {
return 1;
}
if ($policy === 'DAILY_LIMIT') {
return 600;
}
$this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [
'exception_class' => get_class($e),
]);
return 10;
}
public function parsePolicy(Throwable $e): ?string
{
if (! method_exists($e, 'getResponseBody')) {
return null;
}
$body = $e->getResponseBody();
if (is_string($body)) {
$body = json_decode($body, true) ?? [];
}
if (! is_array($body)) {
return null;
}
$policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;
return is_string($policy) ? strtoupper($policy) : null;
}
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
);
}
/**
* Execute a search request against HubSpot CRM objects with rate limiting.
*
* @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')
* @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.
* @return array The search response with 'results', 'total', 'paging' keys
* @throws RateLimitException When rate limit is hit
* @throws HubspotException On API errors
*/
public function search(string $objectType, array $payload): array
{
$endpoint = self::BASE_URL . "/crm/v3/objects/{$objectType}/search";
return $this->executeRequest(function () use ($endpoint, $payload) {
$response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
return $response->toArray();
});
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$deal = $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') {
return $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
return $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
}
/**
* @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 (RateLimitException $e) {
// throw $e;
} 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);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
17920
|
781
|
37
|
2026-05-11T10:41:35.655527+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778496095655_m2.jpg...
|
PhpStorm
|
faVsco.js – Client.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
3
68
3
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\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 Illuminate\Support\Facades\Redis;
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)
{
$cacheKey = $this->getRateLimitCacheKey();
$cachedRetryAfter = Redis::get($cacheKey);
if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {
throw new RateLimitException(
'Hubspot rate limit (cached circuit-breaker)',
(int) $cachedRetryAfter,
);
}
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'policy' => $this->parsePolicy($e),
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function getRateLimitCacheKey(): string
{
return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());
}
public function isHubspotRateLimit(Throwable $e): bool
{
if ($e instanceof BadRequest
|| $e instanceof DealApiException
|| $e instanceof ContactApiException
|| $e instanceof CompanyApiException
|| $e instanceof \GuzzleHttp\Exception\RequestException
) {
return (int) $e->getCode() === 429;
}
return false;
}
public function parseRetryAfter(Throwable $e): int
{
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$e ' . PHP_EOL . print_r($e, true));
return 5;
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;
}
}
$policy = $this->parsePolicy($e);
if ($policy === 'TEN_SECONDLY_ROLLING') {
return 10;
}
if ($policy === 'SECONDLY') {
return 1;
}
if ($policy === 'DAILY_LIMIT') {
return 600;
}
$this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [
'exception_class' => get_class($e),
]);
return 10;
}
public function parsePolicy(Throwable $e): ?string
{
if (! method_exists($e, 'getResponseBody')) {
return null;
}
$body = $e->getResponseBody();
if (is_string($body)) {
$body = json_decode($body, true) ?? [];
}
if (! is_array($body)) {
return null;
}
$policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;
return is_string($policy) ? strtoupper($policy) : null;
}
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
);
}
/**
* Execute a search request against HubSpot CRM objects with rate limiting.
*
* @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')
* @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.
* @return array The search response with 'results', 'total', 'paging' keys
* @throws RateLimitException When rate limit is hit
* @throws HubspotException On API errors
*/
public function search(string $objectType, array $payload): array
{
$endpoint = self::BASE_URL . "/crm/v3/objects/{$objectType}/search";
return $this->executeRequest(function () use ($endpoint, $payload) {
$response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
return $response->toArray();
});
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$deal = $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') {
return $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
return $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
}
/**
* @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 (RateLimitException $e) {
// throw $e;
} 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);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
19...
|
[{"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":"JY-20725-handle-HS-search-rate-limit, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.09541223,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: JY-20725-handle-HS-search-rate-limit","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.82413566,"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":"HandleHubspotRateLimitTest","depth":6,"bounds":{"left":0.8394282,"top":0.019952115,"width":0.076130316,"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 'HandleHubspotRateLimitTest'","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 'HandleHubspotRateLimitTest'","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":"AXStaticText","text":"3","depth":4,"bounds":{"left":0.5515292,"top":0.15003991,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"68","depth":4,"bounds":{"left":0.56150264,"top":0.15003991,"width":0.010305851,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"3","depth":4,"bounds":{"left":0.5738032,"top":0.15003991,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.5834442,"top":0.14844373,"width":0.00731383,"height":0.018355945},"on_screen":true,"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.59075797,"top":0.14844373,"width":0.006981383,"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\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\\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 Illuminate\\Support\\Facades\\Redis;\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\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Reacts to a rate limits (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 $cacheKey = $this->getRateLimitCacheKey();\n\n $cachedRetryAfter = Redis::get($cacheKey);\n if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n (int) $cachedRetryAfter,\n );\n }\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);\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 'policy' => $this->parsePolicy($e),\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 getRateLimitCacheKey(): string\n {\n return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());\n }\n\n public function isHubspotRateLimit(Throwable $e): bool\n {\n if ($e instanceof BadRequest\n || $e instanceof DealApiException\n || $e instanceof ContactApiException\n || $e instanceof CompanyApiException\n || $e instanceof \\GuzzleHttp\\Exception\\RequestException\n ) {\n return (int) $e->getCode() === 429;\n }\n\n return false;\n }\n\n public function parseRetryAfter(Throwable $e): int\n {\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$e ' . PHP_EOL . print_r($e, true));\n return 5;\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 $policy = $this->parsePolicy($e);\n if ($policy === 'TEN_SECONDLY_ROLLING') {\n return 10;\n }\n if ($policy === 'SECONDLY') {\n return 1;\n }\n if ($policy === 'DAILY_LIMIT') {\n return 600;\n }\n\n $this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [\n 'exception_class' => get_class($e),\n ]);\n\n return 10;\n }\n\n public function parsePolicy(Throwable $e): ?string\n {\n if (! method_exists($e, 'getResponseBody')) {\n return null;\n }\n\n $body = $e->getResponseBody();\n if (is_string($body)) {\n $body = json_decode($body, true) ?? [];\n }\n\n if (! is_array($body)) {\n return null;\n }\n\n $policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;\n\n return is_string($policy) ? strtoupper($policy) : null;\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 * Execute a search request against HubSpot CRM objects with rate limiting.\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n * @return array The search response with 'results', 'total', 'paging' keys\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n */\n public function search(string $objectType, array $payload): array\n {\n $endpoint = self::BASE_URL . \"/crm/v3/objects/{$objectType}/search\";\n\n return $this->executeRequest(function () use ($endpoint, $payload) {\n $response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n return $response->toArray();\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 $deal = $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 return $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n return $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\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 (RateLimitException $e) {\n// throw $e;\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\\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 Illuminate\\Support\\Facades\\Redis;\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\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Reacts to a rate limits (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 $cacheKey = $this->getRateLimitCacheKey();\n\n $cachedRetryAfter = Redis::get($cacheKey);\n if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n (int) $cachedRetryAfter,\n );\n }\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);\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 'policy' => $this->parsePolicy($e),\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 getRateLimitCacheKey(): string\n {\n return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());\n }\n\n public function isHubspotRateLimit(Throwable $e): bool\n {\n if ($e instanceof BadRequest\n || $e instanceof DealApiException\n || $e instanceof ContactApiException\n || $e instanceof CompanyApiException\n || $e instanceof \\GuzzleHttp\\Exception\\RequestException\n ) {\n return (int) $e->getCode() === 429;\n }\n\n return false;\n }\n\n public function parseRetryAfter(Throwable $e): int\n {\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$e ' . PHP_EOL . print_r($e, true));\n return 5;\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 $policy = $this->parsePolicy($e);\n if ($policy === 'TEN_SECONDLY_ROLLING') {\n return 10;\n }\n if ($policy === 'SECONDLY') {\n return 1;\n }\n if ($policy === 'DAILY_LIMIT') {\n return 600;\n }\n\n $this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [\n 'exception_class' => get_class($e),\n ]);\n\n return 10;\n }\n\n public function parsePolicy(Throwable $e): ?string\n {\n if (! method_exists($e, 'getResponseBody')) {\n return null;\n }\n\n $body = $e->getResponseBody();\n if (is_string($body)) {\n $body = json_decode($body, true) ?? [];\n }\n\n if (! is_array($body)) {\n return null;\n }\n\n $policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;\n\n return is_string($policy) ? strtoupper($policy) : null;\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 * Execute a search request against HubSpot CRM objects with rate limiting.\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n * @return array The search response with 'results', 'total', 'paging' keys\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n */\n public function search(string $objectType, array $payload): array\n {\n $endpoint = self::BASE_URL . \"/crm/v3/objects/{$objectType}/search\";\n\n return $this->executeRequest(function () use ($endpoint, $payload) {\n $response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n return $response->toArray();\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 $deal = $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 return $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n return $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\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 (RateLimitException $e) {\n// throw $e;\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":"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":"19","depth":4,"bounds":{"left":0.96276593,"top":0.07581804,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"}]...
|
2407769031564044197
|
5225697106764630116
|
visual_change
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
3
68
3
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\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 Illuminate\Support\Facades\Redis;
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)
{
$cacheKey = $this->getRateLimitCacheKey();
$cachedRetryAfter = Redis::get($cacheKey);
if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {
throw new RateLimitException(
'Hubspot rate limit (cached circuit-breaker)',
(int) $cachedRetryAfter,
);
}
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'policy' => $this->parsePolicy($e),
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function getRateLimitCacheKey(): string
{
return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());
}
public function isHubspotRateLimit(Throwable $e): bool
{
if ($e instanceof BadRequest
|| $e instanceof DealApiException
|| $e instanceof ContactApiException
|| $e instanceof CompanyApiException
|| $e instanceof \GuzzleHttp\Exception\RequestException
) {
return (int) $e->getCode() === 429;
}
return false;
}
public function parseRetryAfter(Throwable $e): int
{
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$e ' . PHP_EOL . print_r($e, true));
return 5;
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;
}
}
$policy = $this->parsePolicy($e);
if ($policy === 'TEN_SECONDLY_ROLLING') {
return 10;
}
if ($policy === 'SECONDLY') {
return 1;
}
if ($policy === 'DAILY_LIMIT') {
return 600;
}
$this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [
'exception_class' => get_class($e),
]);
return 10;
}
public function parsePolicy(Throwable $e): ?string
{
if (! method_exists($e, 'getResponseBody')) {
return null;
}
$body = $e->getResponseBody();
if (is_string($body)) {
$body = json_decode($body, true) ?? [];
}
if (! is_array($body)) {
return null;
}
$policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;
return is_string($policy) ? strtoupper($policy) : null;
}
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
);
}
/**
* Execute a search request against HubSpot CRM objects with rate limiting.
*
* @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')
* @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.
* @return array The search response with 'results', 'total', 'paging' keys
* @throws RateLimitException When rate limit is hit
* @throws HubspotException On API errors
*/
public function search(string $objectType, array $payload): array
{
$endpoint = self::BASE_URL . "/crm/v3/objects/{$objectType}/search";
return $this->executeRequest(function () use ($endpoint, $payload) {
$response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
return $response->toArray();
});
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$deal = $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') {
return $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
return $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
}
/**
* @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 (RateLimitException $e) {
// throw $e;
} 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);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
19...
|
17918
|
NULL
|
NULL
|
NULL
|
|
17921
|
780
|
39
|
2026-05-11T10:41:37.219655+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778496097219_m1.jpg...
|
PhpStorm
|
faVsco.js – Client.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
3
68
3
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\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 Illuminate\Support\Facades\Redis;
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)
{
$cacheKey = $this->getRateLimitCacheKey();
$cachedRetryAfter = Redis::get($cacheKey);
if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {
throw new RateLimitException(
'Hubspot rate limit (cached circuit-breaker)',
(int) $cachedRetryAfter,
);
}
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'policy' => $this->parsePolicy($e),
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function getRateLimitCacheKey(): string
{
return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());
}
public function isHubspotRateLimit(Throwable $e): bool
{
if ($e instanceof BadRequest
|| $e instanceof DealApiException
|| $e instanceof ContactApiException
|| $e instanceof CompanyApiException
|| $e instanceof \GuzzleHttp\Exception\RequestException
) {
return (int) $e->getCode() === 429;
}
return false;
}
public function parseRetryAfter(Throwable $e): int
{
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$e ' . PHP_EOL . print_r($e, true));
return 5;
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;
}
}
$policy = $this->parsePolicy($e);
if ($policy === 'TEN_SECONDLY_ROLLING') {
return 10;
}
if ($policy === 'SECONDLY') {
return 1;
}
if ($policy === 'DAILY_LIMIT') {
return 600;
}
$this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [
'exception_class' => get_class($e),
]);
return 10;
}
public function parsePolicy(Throwable $e): ?string
{
if (! method_exists($e, 'getResponseBody')) {
return null;
}
$body = $e->getResponseBody();
if (is_string($body)) {
$body = json_decode($body, true) ?? [];
}
if (! is_array($body)) {
return null;
}
$policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;
return is_string($policy) ? strtoupper($policy) : null;
}
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
);
}
/**
* Execute a search request against HubSpot CRM objects with rate limiting.
*
* @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')
* @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.
* @return array The search response with 'results', 'total', 'paging' keys
* @throws RateLimitException When rate limit is hit
* @throws HubspotException On API errors
*/
public function search(string $objectType, array $payload): array
{
$endpoint = self::BASE_URL . "/crm/v3/objects/{$objectType}/search";
return $this->executeRequest(function () use ($endpoint, $payload) {
$response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
return $response->toArray();
});
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$deal = $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') {
return $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
return $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
}
/**
* @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 (RateLimitException $e) {
// throw $e;
} 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);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
Project
Project...
|
[{"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":"JY-20725-handle-HS-search-rate-limit, menu","depth":5,"on_screen":true,"help_text":"Git Branch: JY-20725-handle-HS-search-rate-limit","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":"HandleHubspotRateLimitTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'HandleHubspotRateLimitTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'HandleHubspotRateLimitTest'","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":"AXStaticText","text":"3","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"68","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"3","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"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\\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 Illuminate\\Support\\Facades\\Redis;\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\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Reacts to a rate limits (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 $cacheKey = $this->getRateLimitCacheKey();\n\n $cachedRetryAfter = Redis::get($cacheKey);\n if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n (int) $cachedRetryAfter,\n );\n }\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);\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 'policy' => $this->parsePolicy($e),\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 getRateLimitCacheKey(): string\n {\n return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());\n }\n\n public function isHubspotRateLimit(Throwable $e): bool\n {\n if ($e instanceof BadRequest\n || $e instanceof DealApiException\n || $e instanceof ContactApiException\n || $e instanceof CompanyApiException\n || $e instanceof \\GuzzleHttp\\Exception\\RequestException\n ) {\n return (int) $e->getCode() === 429;\n }\n\n return false;\n }\n\n public function parseRetryAfter(Throwable $e): int\n {\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$e ' . PHP_EOL . print_r($e, true));\n return 5;\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 $policy = $this->parsePolicy($e);\n if ($policy === 'TEN_SECONDLY_ROLLING') {\n return 10;\n }\n if ($policy === 'SECONDLY') {\n return 1;\n }\n if ($policy === 'DAILY_LIMIT') {\n return 600;\n }\n\n $this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [\n 'exception_class' => get_class($e),\n ]);\n\n return 10;\n }\n\n public function parsePolicy(Throwable $e): ?string\n {\n if (! method_exists($e, 'getResponseBody')) {\n return null;\n }\n\n $body = $e->getResponseBody();\n if (is_string($body)) {\n $body = json_decode($body, true) ?? [];\n }\n\n if (! is_array($body)) {\n return null;\n }\n\n $policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;\n\n return is_string($policy) ? strtoupper($policy) : null;\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 * Execute a search request against HubSpot CRM objects with rate limiting.\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n * @return array The search response with 'results', 'total', 'paging' keys\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n */\n public function search(string $objectType, array $payload): array\n {\n $endpoint = self::BASE_URL . \"/crm/v3/objects/{$objectType}/search\";\n\n return $this->executeRequest(function () use ($endpoint, $payload) {\n $response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n return $response->toArray();\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 $deal = $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 return $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n return $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\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 (RateLimitException $e) {\n// throw $e;\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\\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 Illuminate\\Support\\Facades\\Redis;\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\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Reacts to a rate limits (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 $cacheKey = $this->getRateLimitCacheKey();\n\n $cachedRetryAfter = Redis::get($cacheKey);\n if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n (int) $cachedRetryAfter,\n );\n }\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);\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 'policy' => $this->parsePolicy($e),\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 getRateLimitCacheKey(): string\n {\n return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());\n }\n\n public function isHubspotRateLimit(Throwable $e): bool\n {\n if ($e instanceof BadRequest\n || $e instanceof DealApiException\n || $e instanceof ContactApiException\n || $e instanceof CompanyApiException\n || $e instanceof \\GuzzleHttp\\Exception\\RequestException\n ) {\n return (int) $e->getCode() === 429;\n }\n\n return false;\n }\n\n public function parseRetryAfter(Throwable $e): int\n {\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$e ' . PHP_EOL . print_r($e, true));\n return 5;\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 $policy = $this->parsePolicy($e);\n if ($policy === 'TEN_SECONDLY_ROLLING') {\n return 10;\n }\n if ($policy === 'SECONDLY') {\n return 1;\n }\n if ($policy === 'DAILY_LIMIT') {\n return 600;\n }\n\n $this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [\n 'exception_class' => get_class($e),\n ]);\n\n return 10;\n }\n\n public function parsePolicy(Throwable $e): ?string\n {\n if (! method_exists($e, 'getResponseBody')) {\n return null;\n }\n\n $body = $e->getResponseBody();\n if (is_string($body)) {\n $body = json_decode($body, true) ?? [];\n }\n\n if (! is_array($body)) {\n return null;\n }\n\n $policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;\n\n return is_string($policy) ? strtoupper($policy) : null;\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 * Execute a search request against HubSpot CRM objects with rate limiting.\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n * @return array The search response with 'results', 'total', 'paging' keys\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n */\n public function search(string $objectType, array $payload): array\n {\n $endpoint = self::BASE_URL . \"/crm/v3/objects/{$objectType}/search\";\n\n return $this->executeRequest(function () use ($endpoint, $payload) {\n $response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n return $response->toArray();\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 $deal = $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 return $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n return $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\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 (RateLimitException $e) {\n// throw $e;\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":"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":"19","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","depth":4,"on_screen":true,"value":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","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}]...
|
-2910083726154500178
|
6378618611371477092
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
3
68
3
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\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 Illuminate\Support\Facades\Redis;
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)
{
$cacheKey = $this->getRateLimitCacheKey();
$cachedRetryAfter = Redis::get($cacheKey);
if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {
throw new RateLimitException(
'Hubspot rate limit (cached circuit-breaker)',
(int) $cachedRetryAfter,
);
}
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'policy' => $this->parsePolicy($e),
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function getRateLimitCacheKey(): string
{
return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());
}
public function isHubspotRateLimit(Throwable $e): bool
{
if ($e instanceof BadRequest
|| $e instanceof DealApiException
|| $e instanceof ContactApiException
|| $e instanceof CompanyApiException
|| $e instanceof \GuzzleHttp\Exception\RequestException
) {
return (int) $e->getCode() === 429;
}
return false;
}
public function parseRetryAfter(Throwable $e): int
{
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$e ' . PHP_EOL . print_r($e, true));
return 5;
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;
}
}
$policy = $this->parsePolicy($e);
if ($policy === 'TEN_SECONDLY_ROLLING') {
return 10;
}
if ($policy === 'SECONDLY') {
return 1;
}
if ($policy === 'DAILY_LIMIT') {
return 600;
}
$this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [
'exception_class' => get_class($e),
]);
return 10;
}
public function parsePolicy(Throwable $e): ?string
{
if (! method_exists($e, 'getResponseBody')) {
return null;
}
$body = $e->getResponseBody();
if (is_string($body)) {
$body = json_decode($body, true) ?? [];
}
if (! is_array($body)) {
return null;
}
$policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;
return is_string($policy) ? strtoupper($policy) : null;
}
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
);
}
/**
* Execute a search request against HubSpot CRM objects with rate limiting.
*
* @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')
* @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.
* @return array The search response with 'results', 'total', 'paging' keys
* @throws RateLimitException When rate limit is hit
* @throws HubspotException On API errors
*/
public function search(string $objectType, array $payload): array
{
$endpoint = self::BASE_URL . "/crm/v3/objects/{$objectType}/search";
return $this->executeRequest(function () use ($endpoint, $payload) {
$response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
return $response->toArray();
});
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$deal = $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') {
return $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
return $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
}
/**
* @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 (RateLimitException $e) {
// throw $e;
} 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);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
Project
Project...
|
17919
|
NULL
|
NULL
|
NULL
|
|
17922
|
781
|
38
|
2026-05-11T10:41:37.253389+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778496097253_m2.jpg...
|
PhpStorm
|
faVsco.js – Client.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
3
68
3
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\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 Illuminate\Support\Facades\Redis;
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)
{
$cacheKey = $this->getRateLimitCacheKey();
$cachedRetryAfter = Redis::get($cacheKey);
if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {
throw new RateLimitException(
'Hubspot rate limit (cached circuit-breaker)',
(int) $cachedRetryAfter,
);
}
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'policy' => $this->parsePolicy($e),
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function getRateLimitCacheKey(): string
{
return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());
}
public function isHubspotRateLimit(Throwable $e): bool
{
if ($e instanceof BadRequest
|| $e instanceof DealApiException
|| $e instanceof ContactApiException
|| $e instanceof CompanyApiException
|| $e instanceof \GuzzleHttp\Exception\RequestException
) {
return (int) $e->getCode() === 429;
}
return false;
}
public function parseRetryAfter(Throwable $e): int
{
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$e ' . PHP_EOL . print_r($e, true));
return 5;
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;
}
}
$policy = $this->parsePolicy($e);
if ($policy === 'TEN_SECONDLY_ROLLING') {
return 10;
}
if ($policy === 'SECONDLY') {
return 1;
}
if ($policy === 'DAILY_LIMIT') {
return 600;
}
$this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [
'exception_class' => get_class($e),
]);
return 10;
}
public function parsePolicy(Throwable $e): ?string
{
if (! method_exists($e, 'getResponseBody')) {
return null;
}
$body = $e->getResponseBody();
if (is_string($body)) {
$body = json_decode($body, true) ?? [];
}
if (! is_array($body)) {
return null;
}
$policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;
return is_string($policy) ? strtoupper($policy) : null;
}
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
);
}
/**
* Execute a search request against HubSpot CRM objects with rate limiting.
*
* @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')
* @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.
* @return array The search response with 'results', 'total', 'paging' keys
* @throws RateLimitException When rate limit is hit
* @throws HubspotException On API errors
*/
public function search(string $objectType, array $payload): array
{
$endpoint = self::BASE_URL . "/crm/v3/objects/{$objectType}/search";
return $this->executeRequest(function () use ($endpoint, $payload) {
$response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
return $response->toArray();
});
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$deal = $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') {
return $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
return $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
}
/**
* @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 (RateLimitException $e) {
// throw $e;
} 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);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}...
|
[{"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":"JY-20725-handle-HS-search-rate-limit, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.09541223,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: JY-20725-handle-HS-search-rate-limit","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.82413566,"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":"HandleHubspotRateLimitTest","depth":6,"bounds":{"left":0.8394282,"top":0.019952115,"width":0.076130316,"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 'HandleHubspotRateLimitTest'","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 'HandleHubspotRateLimitTest'","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":"AXStaticText","text":"3","depth":4,"bounds":{"left":0.5515292,"top":0.15003991,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"68","depth":4,"bounds":{"left":0.56150264,"top":0.15003991,"width":0.010305851,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"3","depth":4,"bounds":{"left":0.5738032,"top":0.15003991,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.5834442,"top":0.14844373,"width":0.00731383,"height":0.018355945},"on_screen":true,"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.59075797,"top":0.14844373,"width":0.006981383,"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\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\\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 Illuminate\\Support\\Facades\\Redis;\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\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Reacts to a rate limits (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 $cacheKey = $this->getRateLimitCacheKey();\n\n $cachedRetryAfter = Redis::get($cacheKey);\n if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n (int) $cachedRetryAfter,\n );\n }\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);\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 'policy' => $this->parsePolicy($e),\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 getRateLimitCacheKey(): string\n {\n return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());\n }\n\n public function isHubspotRateLimit(Throwable $e): bool\n {\n if ($e instanceof BadRequest\n || $e instanceof DealApiException\n || $e instanceof ContactApiException\n || $e instanceof CompanyApiException\n || $e instanceof \\GuzzleHttp\\Exception\\RequestException\n ) {\n return (int) $e->getCode() === 429;\n }\n\n return false;\n }\n\n public function parseRetryAfter(Throwable $e): int\n {\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$e ' . PHP_EOL . print_r($e, true));\n return 5;\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 $policy = $this->parsePolicy($e);\n if ($policy === 'TEN_SECONDLY_ROLLING') {\n return 10;\n }\n if ($policy === 'SECONDLY') {\n return 1;\n }\n if ($policy === 'DAILY_LIMIT') {\n return 600;\n }\n\n $this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [\n 'exception_class' => get_class($e),\n ]);\n\n return 10;\n }\n\n public function parsePolicy(Throwable $e): ?string\n {\n if (! method_exists($e, 'getResponseBody')) {\n return null;\n }\n\n $body = $e->getResponseBody();\n if (is_string($body)) {\n $body = json_decode($body, true) ?? [];\n }\n\n if (! is_array($body)) {\n return null;\n }\n\n $policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;\n\n return is_string($policy) ? strtoupper($policy) : null;\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 * Execute a search request against HubSpot CRM objects with rate limiting.\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n * @return array The search response with 'results', 'total', 'paging' keys\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n */\n public function search(string $objectType, array $payload): array\n {\n $endpoint = self::BASE_URL . \"/crm/v3/objects/{$objectType}/search\";\n\n return $this->executeRequest(function () use ($endpoint, $payload) {\n $response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n return $response->toArray();\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 $deal = $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 return $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n return $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\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 (RateLimitException $e) {\n// throw $e;\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\\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 Illuminate\\Support\\Facades\\Redis;\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\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Reacts to a rate limits (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 $cacheKey = $this->getRateLimitCacheKey();\n\n $cachedRetryAfter = Redis::get($cacheKey);\n if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n (int) $cachedRetryAfter,\n );\n }\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);\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 'policy' => $this->parsePolicy($e),\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 getRateLimitCacheKey(): string\n {\n return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());\n }\n\n public function isHubspotRateLimit(Throwable $e): bool\n {\n if ($e instanceof BadRequest\n || $e instanceof DealApiException\n || $e instanceof ContactApiException\n || $e instanceof CompanyApiException\n || $e instanceof \\GuzzleHttp\\Exception\\RequestException\n ) {\n return (int) $e->getCode() === 429;\n }\n\n return false;\n }\n\n public function parseRetryAfter(Throwable $e): int\n {\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$e ' . PHP_EOL . print_r($e, true));\n return 5;\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 $policy = $this->parsePolicy($e);\n if ($policy === 'TEN_SECONDLY_ROLLING') {\n return 10;\n }\n if ($policy === 'SECONDLY') {\n return 1;\n }\n if ($policy === 'DAILY_LIMIT') {\n return 600;\n }\n\n $this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [\n 'exception_class' => get_class($e),\n ]);\n\n return 10;\n }\n\n public function parsePolicy(Throwable $e): ?string\n {\n if (! method_exists($e, 'getResponseBody')) {\n return null;\n }\n\n $body = $e->getResponseBody();\n if (is_string($body)) {\n $body = json_decode($body, true) ?? [];\n }\n\n if (! is_array($body)) {\n return null;\n }\n\n $policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;\n\n return is_string($policy) ? strtoupper($policy) : null;\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 * Execute a search request against HubSpot CRM objects with rate limiting.\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n * @return array The search response with 'results', 'total', 'paging' keys\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n */\n public function search(string $objectType, array $payload): array\n {\n $endpoint = self::BASE_URL . \"/crm/v3/objects/{$objectType}/search\";\n\n return $this->executeRequest(function () use ($endpoint, $payload) {\n $response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n return $response->toArray();\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 $deal = $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 return $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n return $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\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 (RateLimitException $e) {\n// throw $e;\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":"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":"19","depth":4,"bounds":{"left":0.96276593,"top":0.07581804,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.9740692,"top":0.074221864,"width":0.00731383,"height":0.018355945},"on_screen":true,"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.98138297,"top":0.074221864,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","depth":4,"bounds":{"left":0.61702126,"top":0.0726257,"width":0.38297874,"height":0.9066241},"on_screen":true,"lines":[{"char_start":273,"char_count":32,"bounds":{"left":0.61702126,"top":0.0,"width":0.080119684,"height":0.014365523}},{"char_start":305,"char_count":79,"bounds":{"left":0.61702126,"top":0.0,"width":0.20212767,"height":0.014365523}},{"char_start":384,"char_count":18,"bounds":{"left":0.61702126,"top":0.0,"width":0.043882977,"height":0.014365523}},{"char_start":402,"char_count":21,"bounds":{"left":0.61702126,"top":0.0,"width":0.051861703,"height":0.014365523}},{"char_start":423,"char_count":48,"bounds":{"left":0.61702126,"top":0.0,"width":0.12167553,"height":0.014365523}},{"char_start":471,"char_count":72,"bounds":{"left":0.61702126,"top":0.0015961692,"width":0.18384309,"height":0.014365523}},{"char_start":543,"char_count":40,"bounds":{"left":0.61702126,"top":0.01915403,"width":0.10106383,"height":0.014365523}},{"char_start":583,"char_count":41,"bounds":{"left":0.61702126,"top":0.03671189,"width":0.10372341,"height":0.014365523}},{"char_start":624,"char_count":72,"bounds":{"left":0.61702126,"top":0.054269753,"width":0.18384309,"height":0.014365523}},{"char_start":696,"char_count":219,"bounds":{"left":0.61702126,"top":0.07182761,"width":0.38297874,"height":0.014365523}},{"char_start":915,"char_count":83,"bounds":{"left":0.61702126,"top":0.08938547,"width":0.21243352,"height":0.014365523}},{"char_start":998,"char_count":20,"bounds":{"left":0.61702126,"top":0.10694334,"width":0.04920213,"height":0.014365523}},{"char_start":1018,"char_count":17,"bounds":{"left":0.61702126,"top":0.1245012,"width":0.041223403,"height":0.014365523}},{"char_start":1035,"char_count":203,"bounds":{"left":0.61702126,"top":0.14205906,"width":0.38297874,"height":0.014365523}},{"char_start":1238,"char_count":22,"bounds":{"left":0.61702126,"top":0.15961692,"width":0.05418883,"height":0.014365523}},{"char_start":1260,"char_count":23,"bounds":{"left":0.61702126,"top":0.17717478,"width":0.056848403,"height":0.014365523}},{"char_start":1283,"char_count":10,"bounds":{"left":0.61702126,"top":0.19473264,"width":0.023271276,"height":0.014365523}},{"char_start":1293,"char_count":27,"bounds":{"left":0.61702126,"top":0.2122905,"width":0.06715426,"height":0.014365523}},{"char_start":1320,"char_count":26,"bounds":{"left":0.61702126,"top":0.22984837,"width":0.06482713,"height":0.014365523}},{"char_start":1346,"char_count":23,"bounds":{"left":0.61702126,"top":0.24740623,"width":0.056848403,"height":0.014365523}},{"char_start":1369,"char_count":28,"bounds":{"left":0.61702126,"top":0.26496407,"width":0.06981383,"height":0.014365523}},{"char_start":1397,"char_count":57,"bounds":{"left":0.61702126,"top":0.28252193,"width":0.14494681,"height":0.014365523}}],"value":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-1006337435740056067
|
6378618611371477092
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
3
68
3
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\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 Illuminate\Support\Facades\Redis;
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)
{
$cacheKey = $this->getRateLimitCacheKey();
$cachedRetryAfter = Redis::get($cacheKey);
if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {
throw new RateLimitException(
'Hubspot rate limit (cached circuit-breaker)',
(int) $cachedRetryAfter,
);
}
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'policy' => $this->parsePolicy($e),
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function getRateLimitCacheKey(): string
{
return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());
}
public function isHubspotRateLimit(Throwable $e): bool
{
if ($e instanceof BadRequest
|| $e instanceof DealApiException
|| $e instanceof ContactApiException
|| $e instanceof CompanyApiException
|| $e instanceof \GuzzleHttp\Exception\RequestException
) {
return (int) $e->getCode() === 429;
}
return false;
}
public function parseRetryAfter(Throwable $e): int
{
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$e ' . PHP_EOL . print_r($e, true));
return 5;
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;
}
}
$policy = $this->parsePolicy($e);
if ($policy === 'TEN_SECONDLY_ROLLING') {
return 10;
}
if ($policy === 'SECONDLY') {
return 1;
}
if ($policy === 'DAILY_LIMIT') {
return 600;
}
$this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [
'exception_class' => get_class($e),
]);
return 10;
}
public function parsePolicy(Throwable $e): ?string
{
if (! method_exists($e, 'getResponseBody')) {
return null;
}
$body = $e->getResponseBody();
if (is_string($body)) {
$body = json_decode($body, true) ?? [];
}
if (! is_array($body)) {
return null;
}
$policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;
return is_string($policy) ? strtoupper($policy) : null;
}
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
);
}
/**
* Execute a search request against HubSpot CRM objects with rate limiting.
*
* @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')
* @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.
* @return array The search response with 'results', 'total', 'paging' keys
* @throws RateLimitException When rate limit is hit
* @throws HubspotException On API errors
*/
public function search(string $objectType, array $payload): array
{
$endpoint = self::BASE_URL . "/crm/v3/objects/{$objectType}/search";
return $this->executeRequest(function () use ($endpoint, $payload) {
$response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
return $response->toArray();
});
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$deal = $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') {
return $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
return $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
}
/**
* @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 (RateLimitException $e) {
// throw $e;
} 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);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
17923
|
780
|
40
|
2026-05-11T10:41:39.079423+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778496099079_m1.jpg...
|
PhpStorm
|
faVsco.js – Client.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
3
68
3
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\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 Illuminate\Support\Facades\Redis;
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)
{
$cacheKey = $this->getRateLimitCacheKey();
$cachedRetryAfter = Redis::get($cacheKey);
if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {
throw new RateLimitException(
'Hubspot rate limit (cached circuit-breaker)',
(int) $cachedRetryAfter,
);
}
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'policy' => $this->parsePolicy($e),
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function getRateLimitCacheKey(): string
{
return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());
}
public function isHubspotRateLimit(Throwable $e): bool
{
if ($e instanceof BadRequest
|| $e instanceof DealApiException
|| $e instanceof ContactApiException
|| $e instanceof CompanyApiException
|| $e instanceof \GuzzleHttp\Exception\RequestException
) {
return (int) $e->getCode() === 429;
}
return false;
}
public function parseRetryAfter(Throwable $e): int
{
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$e ' . PHP_EOL . print_r($e, true));
return 5;
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;
}
}
$policy = $this->parsePolicy($e);
if ($policy === 'TEN_SECONDLY_ROLLING') {
return 10;
}
if ($policy === 'SECONDLY') {
return 1;
}
if ($policy === 'DAILY_LIMIT') {
return 600;
}
$this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [
'exception_class' => get_class($e),
]);
return 10;
}
public function parsePolicy(Throwable $e): ?string
{
if (! method_exists($e, 'getResponseBody')) {
return null;
}
$body = $e->getResponseBody();
if (is_string($body)) {
$body = json_decode($body, true) ?? [];
}
if (! is_array($body)) {
return null;
}
$policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;
return is_string($policy) ? strtoupper($policy) : null;
}
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
);
}
/**
* Execute a search request against HubSpot CRM objects with rate limiting.
*
* @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')
* @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.
* @return array The search response with 'results', 'total', 'paging' keys
* @throws RateLimitException When rate limit is hit
* @throws HubspotException On API errors
*/
public function search(string $objectType, array $payload): array
{
$endpoint = self::BASE_URL . "/crm/v3/objects/{$objectType}/search";
return $this->executeRequest(function () use ($endpoint, $payload) {
$response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
return $response->toArray();
});
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$deal = $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') {
return $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
return $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
}
/**
* @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 (RateLimitException $e) {
// throw $e;
} 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);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
Project
Project
New File or Directory…
Expand Selected
Collapse All...
|
[{"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":"JY-20725-handle-HS-search-rate-limit, menu","depth":5,"on_screen":true,"help_text":"Git Branch: JY-20725-handle-HS-search-rate-limit","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":"HandleHubspotRateLimitTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'HandleHubspotRateLimitTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'HandleHubspotRateLimitTest'","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":"AXStaticText","text":"3","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"68","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"3","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"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\\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 Illuminate\\Support\\Facades\\Redis;\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\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Reacts to a rate limits (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 $cacheKey = $this->getRateLimitCacheKey();\n\n $cachedRetryAfter = Redis::get($cacheKey);\n if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n (int) $cachedRetryAfter,\n );\n }\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);\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 'policy' => $this->parsePolicy($e),\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 getRateLimitCacheKey(): string\n {\n return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());\n }\n\n public function isHubspotRateLimit(Throwable $e): bool\n {\n if ($e instanceof BadRequest\n || $e instanceof DealApiException\n || $e instanceof ContactApiException\n || $e instanceof CompanyApiException\n || $e instanceof \\GuzzleHttp\\Exception\\RequestException\n ) {\n return (int) $e->getCode() === 429;\n }\n\n return false;\n }\n\n public function parseRetryAfter(Throwable $e): int\n {\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$e ' . PHP_EOL . print_r($e, true));\n return 5;\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 $policy = $this->parsePolicy($e);\n if ($policy === 'TEN_SECONDLY_ROLLING') {\n return 10;\n }\n if ($policy === 'SECONDLY') {\n return 1;\n }\n if ($policy === 'DAILY_LIMIT') {\n return 600;\n }\n\n $this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [\n 'exception_class' => get_class($e),\n ]);\n\n return 10;\n }\n\n public function parsePolicy(Throwable $e): ?string\n {\n if (! method_exists($e, 'getResponseBody')) {\n return null;\n }\n\n $body = $e->getResponseBody();\n if (is_string($body)) {\n $body = json_decode($body, true) ?? [];\n }\n\n if (! is_array($body)) {\n return null;\n }\n\n $policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;\n\n return is_string($policy) ? strtoupper($policy) : null;\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 * Execute a search request against HubSpot CRM objects with rate limiting.\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n * @return array The search response with 'results', 'total', 'paging' keys\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n */\n public function search(string $objectType, array $payload): array\n {\n $endpoint = self::BASE_URL . \"/crm/v3/objects/{$objectType}/search\";\n\n return $this->executeRequest(function () use ($endpoint, $payload) {\n $response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n return $response->toArray();\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 $deal = $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 return $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n return $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\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 (RateLimitException $e) {\n// throw $e;\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\\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 Illuminate\\Support\\Facades\\Redis;\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\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Reacts to a rate limits (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 $cacheKey = $this->getRateLimitCacheKey();\n\n $cachedRetryAfter = Redis::get($cacheKey);\n if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n (int) $cachedRetryAfter,\n );\n }\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);\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 'policy' => $this->parsePolicy($e),\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 getRateLimitCacheKey(): string\n {\n return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());\n }\n\n public function isHubspotRateLimit(Throwable $e): bool\n {\n if ($e instanceof BadRequest\n || $e instanceof DealApiException\n || $e instanceof ContactApiException\n || $e instanceof CompanyApiException\n || $e instanceof \\GuzzleHttp\\Exception\\RequestException\n ) {\n return (int) $e->getCode() === 429;\n }\n\n return false;\n }\n\n public function parseRetryAfter(Throwable $e): int\n {\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$e ' . PHP_EOL . print_r($e, true));\n return 5;\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 $policy = $this->parsePolicy($e);\n if ($policy === 'TEN_SECONDLY_ROLLING') {\n return 10;\n }\n if ($policy === 'SECONDLY') {\n return 1;\n }\n if ($policy === 'DAILY_LIMIT') {\n return 600;\n }\n\n $this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [\n 'exception_class' => get_class($e),\n ]);\n\n return 10;\n }\n\n public function parsePolicy(Throwable $e): ?string\n {\n if (! method_exists($e, 'getResponseBody')) {\n return null;\n }\n\n $body = $e->getResponseBody();\n if (is_string($body)) {\n $body = json_decode($body, true) ?? [];\n }\n\n if (! is_array($body)) {\n return null;\n }\n\n $policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;\n\n return is_string($policy) ? strtoupper($policy) : null;\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 * Execute a search request against HubSpot CRM objects with rate limiting.\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n * @return array The search response with 'results', 'total', 'paging' keys\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n */\n public function search(string $objectType, array $payload): array\n {\n $endpoint = self::BASE_URL . \"/crm/v3/objects/{$objectType}/search\";\n\n return $this->executeRequest(function () use ($endpoint, $payload) {\n $response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n return $response->toArray();\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 $deal = $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 return $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n return $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\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 (RateLimitException $e) {\n// throw $e;\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":"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":"19","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","depth":4,"on_screen":true,"value":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","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}]...
|
7623778648713633268
|
6378618611371477092
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
3
68
3
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\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 Illuminate\Support\Facades\Redis;
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)
{
$cacheKey = $this->getRateLimitCacheKey();
$cachedRetryAfter = Redis::get($cacheKey);
if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {
throw new RateLimitException(
'Hubspot rate limit (cached circuit-breaker)',
(int) $cachedRetryAfter,
);
}
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'policy' => $this->parsePolicy($e),
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function getRateLimitCacheKey(): string
{
return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());
}
public function isHubspotRateLimit(Throwable $e): bool
{
if ($e instanceof BadRequest
|| $e instanceof DealApiException
|| $e instanceof ContactApiException
|| $e instanceof CompanyApiException
|| $e instanceof \GuzzleHttp\Exception\RequestException
) {
return (int) $e->getCode() === 429;
}
return false;
}
public function parseRetryAfter(Throwable $e): int
{
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$e ' . PHP_EOL . print_r($e, true));
return 5;
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;
}
}
$policy = $this->parsePolicy($e);
if ($policy === 'TEN_SECONDLY_ROLLING') {
return 10;
}
if ($policy === 'SECONDLY') {
return 1;
}
if ($policy === 'DAILY_LIMIT') {
return 600;
}
$this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [
'exception_class' => get_class($e),
]);
return 10;
}
public function parsePolicy(Throwable $e): ?string
{
if (! method_exists($e, 'getResponseBody')) {
return null;
}
$body = $e->getResponseBody();
if (is_string($body)) {
$body = json_decode($body, true) ?? [];
}
if (! is_array($body)) {
return null;
}
$policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;
return is_string($policy) ? strtoupper($policy) : null;
}
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
);
}
/**
* Execute a search request against HubSpot CRM objects with rate limiting.
*
* @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')
* @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.
* @return array The search response with 'results', 'total', 'paging' keys
* @throws RateLimitException When rate limit is hit
* @throws HubspotException On API errors
*/
public function search(string $objectType, array $payload): array
{
$endpoint = self::BASE_URL . "/crm/v3/objects/{$objectType}/search";
return $this->executeRequest(function () use ($endpoint, $payload) {
$response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
return $response->toArray();
});
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$deal = $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') {
return $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
return $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
}
/**
* @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 (RateLimitException $e) {
// throw $e;
} 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);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
Project
Project
New File or Directory…
Expand Selected
Collapse All...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
17924
|
781
|
39
|
2026-05-11T10:41:44.689116+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778496104689_m2.jpg...
|
PhpStorm
|
faVsco.js – Client.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
3
68
3
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\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 Illuminate\Support\Facades\Redis;
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)
{
$cacheKey = $this->getRateLimitCacheKey();
$cachedRetryAfter = Redis::get($cacheKey);
if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {
throw new RateLimitException(
'Hubspot rate limit (cached circuit-breaker)',
(int) $cachedRetryAfter,
);
}
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'policy' => $this->parsePolicy($e),
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function getRateLimitCacheKey(): string
{
return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());
}
public function isHubspotRateLimit(Throwable $e): bool
{
if ($e instanceof BadRequest
|| $e instanceof DealApiException
|| $e instanceof ContactApiException
|| $e instanceof CompanyApiException
|| $e instanceof \GuzzleHttp\Exception\RequestException
) {
return (int) $e->getCode() === 429;
}
return false;
}
public function parseRetryAfter(Throwable $e): int
{
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$e ' . PHP_EOL . print_r($e, true));
return 5;
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;
}
}
$policy = $this->parsePolicy($e);
if ($policy === 'TEN_SECONDLY_ROLLING') {
return 10;
}
if ($policy === 'SECONDLY') {
return 1;
}
if ($policy === 'DAILY_LIMIT') {
return 600;
}
$this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [
'exception_class' => get_class($e),
]);
return 10;
}
public function parsePolicy(Throwable $e): ?string
{
if (! method_exists($e, 'getResponseBody')) {
return null;
}
$body = $e->getResponseBody();
if (is_string($body)) {
$body = json_decode($body, true) ?? [];
}
if (! is_array($body)) {
return null;
}
$policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;
return is_string($policy) ? strtoupper($policy) : null;
}
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
);
}
/**
* Execute a search request against HubSpot CRM objects with rate limiting.
*
* @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')
* @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.
* @return array The search response with 'results', 'total', 'paging' keys
* @throws RateLimitException When rate limit is hit
* @throws HubspotException On API errors
*/
public function search(string $objectType, array $payload): array
{
$endpoint = self::BASE_URL . "/crm/v3/objects/{$objectType}/search";
return $this->executeRequest(function () use ($endpoint, $payload) {
$response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
return $response->toArray();
});
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$deal = $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') {
return $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
return $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
}
/**
* @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 (RateLimitException $e) {
// throw $e;
} 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);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
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":"JY-20725-handle-HS-search-rate-limit, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.09541223,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: JY-20725-handle-HS-search-rate-limit","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.82413566,"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":"HandleHubspotRateLimitTest","depth":6,"bounds":{"left":0.8394282,"top":0.019952115,"width":0.076130316,"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 'HandleHubspotRateLimitTest'","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 'HandleHubspotRateLimitTest'","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":"AXStaticText","text":"3","depth":4,"bounds":{"left":0.5515292,"top":0.15003991,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"68","depth":4,"bounds":{"left":0.56150264,"top":0.15003991,"width":0.010305851,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"3","depth":4,"bounds":{"left":0.5738032,"top":0.15003991,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.5834442,"top":0.14844373,"width":0.00731383,"height":0.018355945},"on_screen":true,"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.59075797,"top":0.14844373,"width":0.006981383,"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\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\\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 Illuminate\\Support\\Facades\\Redis;\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\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Reacts to a rate limits (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 $cacheKey = $this->getRateLimitCacheKey();\n\n $cachedRetryAfter = Redis::get($cacheKey);\n if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n (int) $cachedRetryAfter,\n );\n }\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);\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 'policy' => $this->parsePolicy($e),\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 getRateLimitCacheKey(): string\n {\n return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());\n }\n\n public function isHubspotRateLimit(Throwable $e): bool\n {\n if ($e instanceof BadRequest\n || $e instanceof DealApiException\n || $e instanceof ContactApiException\n || $e instanceof CompanyApiException\n || $e instanceof \\GuzzleHttp\\Exception\\RequestException\n ) {\n return (int) $e->getCode() === 429;\n }\n\n return false;\n }\n\n public function parseRetryAfter(Throwable $e): int\n {\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$e ' . PHP_EOL . print_r($e, true));\n return 5;\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 $policy = $this->parsePolicy($e);\n if ($policy === 'TEN_SECONDLY_ROLLING') {\n return 10;\n }\n if ($policy === 'SECONDLY') {\n return 1;\n }\n if ($policy === 'DAILY_LIMIT') {\n return 600;\n }\n\n $this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [\n 'exception_class' => get_class($e),\n ]);\n\n return 10;\n }\n\n public function parsePolicy(Throwable $e): ?string\n {\n if (! method_exists($e, 'getResponseBody')) {\n return null;\n }\n\n $body = $e->getResponseBody();\n if (is_string($body)) {\n $body = json_decode($body, true) ?? [];\n }\n\n if (! is_array($body)) {\n return null;\n }\n\n $policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;\n\n return is_string($policy) ? strtoupper($policy) : null;\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 * Execute a search request against HubSpot CRM objects with rate limiting.\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n * @return array The search response with 'results', 'total', 'paging' keys\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n */\n public function search(string $objectType, array $payload): array\n {\n $endpoint = self::BASE_URL . \"/crm/v3/objects/{$objectType}/search\";\n\n return $this->executeRequest(function () use ($endpoint, $payload) {\n $response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n return $response->toArray();\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 $deal = $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 return $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n return $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\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 (RateLimitException $e) {\n// throw $e;\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\\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 Illuminate\\Support\\Facades\\Redis;\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\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Reacts to a rate limits (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 $cacheKey = $this->getRateLimitCacheKey();\n\n $cachedRetryAfter = Redis::get($cacheKey);\n if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n (int) $cachedRetryAfter,\n );\n }\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);\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 'policy' => $this->parsePolicy($e),\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 getRateLimitCacheKey(): string\n {\n return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());\n }\n\n public function isHubspotRateLimit(Throwable $e): bool\n {\n if ($e instanceof BadRequest\n || $e instanceof DealApiException\n || $e instanceof ContactApiException\n || $e instanceof CompanyApiException\n || $e instanceof \\GuzzleHttp\\Exception\\RequestException\n ) {\n return (int) $e->getCode() === 429;\n }\n\n return false;\n }\n\n public function parseRetryAfter(Throwable $e): int\n {\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$e ' . PHP_EOL . print_r($e, true));\n return 5;\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 $policy = $this->parsePolicy($e);\n if ($policy === 'TEN_SECONDLY_ROLLING') {\n return 10;\n }\n if ($policy === 'SECONDLY') {\n return 1;\n }\n if ($policy === 'DAILY_LIMIT') {\n return 600;\n }\n\n $this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [\n 'exception_class' => get_class($e),\n ]);\n\n return 10;\n }\n\n public function parsePolicy(Throwable $e): ?string\n {\n if (! method_exists($e, 'getResponseBody')) {\n return null;\n }\n\n $body = $e->getResponseBody();\n if (is_string($body)) {\n $body = json_decode($body, true) ?? [];\n }\n\n if (! is_array($body)) {\n return null;\n }\n\n $policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;\n\n return is_string($policy) ? strtoupper($policy) : null;\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 * Execute a search request against HubSpot CRM objects with rate limiting.\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n * @return array The search response with 'results', 'total', 'paging' keys\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n */\n public function search(string $objectType, array $payload): array\n {\n $endpoint = self::BASE_URL . \"/crm/v3/objects/{$objectType}/search\";\n\n return $this->executeRequest(function () use ($endpoint, $payload) {\n $response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n return $response->toArray();\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 $deal = $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 return $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n return $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\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 (RateLimitException $e) {\n// throw $e;\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":"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":"19","depth":4,"bounds":{"left":0.96276593,"top":0.07581804,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.9740692,"top":0.074221864,"width":0.00731383,"height":0.018355945},"on_screen":true,"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.98138297,"top":0.074221864,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","depth":4,"bounds":{"left":0.61702126,"top":0.0726257,"width":0.38297874,"height":0.9066241},"on_screen":true,"lines":[{"char_start":273,"char_count":32,"bounds":{"left":0.61702126,"top":0.0,"width":0.080119684,"height":0.014365523}},{"char_start":305,"char_count":79,"bounds":{"left":0.61702126,"top":0.0,"width":0.20212767,"height":0.014365523}},{"char_start":384,"char_count":18,"bounds":{"left":0.61702126,"top":0.0,"width":0.043882977,"height":0.014365523}},{"char_start":402,"char_count":21,"bounds":{"left":0.61702126,"top":0.0,"width":0.051861703,"height":0.014365523}},{"char_start":423,"char_count":48,"bounds":{"left":0.61702126,"top":0.0,"width":0.12167553,"height":0.014365523}},{"char_start":471,"char_count":72,"bounds":{"left":0.61702126,"top":0.0015961692,"width":0.18384309,"height":0.014365523}},{"char_start":543,"char_count":40,"bounds":{"left":0.61702126,"top":0.01915403,"width":0.10106383,"height":0.014365523}},{"char_start":583,"char_count":41,"bounds":{"left":0.61702126,"top":0.03671189,"width":0.10372341,"height":0.014365523}},{"char_start":624,"char_count":72,"bounds":{"left":0.61702126,"top":0.054269753,"width":0.18384309,"height":0.014365523}},{"char_start":696,"char_count":219,"bounds":{"left":0.61702126,"top":0.07182761,"width":0.38297874,"height":0.014365523}},{"char_start":915,"char_count":83,"bounds":{"left":0.61702126,"top":0.08938547,"width":0.21243352,"height":0.014365523}},{"char_start":998,"char_count":20,"bounds":{"left":0.61702126,"top":0.10694334,"width":0.04920213,"height":0.014365523}},{"char_start":1018,"char_count":17,"bounds":{"left":0.61702126,"top":0.1245012,"width":0.041223403,"height":0.014365523}},{"char_start":1035,"char_count":203,"bounds":{"left":0.61702126,"top":0.14205906,"width":0.38297874,"height":0.014365523}},{"char_start":1238,"char_count":22,"bounds":{"left":0.61702126,"top":0.15961692,"width":0.05418883,"height":0.014365523}},{"char_start":1260,"char_count":23,"bounds":{"left":0.61702126,"top":0.17717478,"width":0.056848403,"height":0.014365523}},{"char_start":1283,"char_count":10,"bounds":{"left":0.61702126,"top":0.19473264,"width":0.023271276,"height":0.014365523}},{"char_start":1293,"char_count":27,"bounds":{"left":0.61702126,"top":0.2122905,"width":0.06715426,"height":0.014365523}},{"char_start":1320,"char_count":26,"bounds":{"left":0.61702126,"top":0.22984837,"width":0.06482713,"height":0.014365523}},{"char_start":1346,"char_count":23,"bounds":{"left":0.61702126,"top":0.24740623,"width":0.056848403,"height":0.014365523}},{"char_start":1369,"char_count":28,"bounds":{"left":0.61702126,"top":0.26496407,"width":0.06981383,"height":0.014365523}},{"char_start":1397,"char_count":57,"bounds":{"left":0.61702126,"top":0.28252193,"width":0.14494681,"height":0.014365523}}],"value":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","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}]...
|
-6243931637791501581
|
6378618611371477092
|
visual_change
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
3
68
3
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\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 Illuminate\Support\Facades\Redis;
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)
{
$cacheKey = $this->getRateLimitCacheKey();
$cachedRetryAfter = Redis::get($cacheKey);
if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {
throw new RateLimitException(
'Hubspot rate limit (cached circuit-breaker)',
(int) $cachedRetryAfter,
);
}
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'policy' => $this->parsePolicy($e),
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function getRateLimitCacheKey(): string
{
return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());
}
public function isHubspotRateLimit(Throwable $e): bool
{
if ($e instanceof BadRequest
|| $e instanceof DealApiException
|| $e instanceof ContactApiException
|| $e instanceof CompanyApiException
|| $e instanceof \GuzzleHttp\Exception\RequestException
) {
return (int) $e->getCode() === 429;
}
return false;
}
public function parseRetryAfter(Throwable $e): int
{
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$e ' . PHP_EOL . print_r($e, true));
return 5;
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;
}
}
$policy = $this->parsePolicy($e);
if ($policy === 'TEN_SECONDLY_ROLLING') {
return 10;
}
if ($policy === 'SECONDLY') {
return 1;
}
if ($policy === 'DAILY_LIMIT') {
return 600;
}
$this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [
'exception_class' => get_class($e),
]);
return 10;
}
public function parsePolicy(Throwable $e): ?string
{
if (! method_exists($e, 'getResponseBody')) {
return null;
}
$body = $e->getResponseBody();
if (is_string($body)) {
$body = json_decode($body, true) ?? [];
}
if (! is_array($body)) {
return null;
}
$policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;
return is_string($policy) ? strtoupper($policy) : null;
}
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
);
}
/**
* Execute a search request against HubSpot CRM objects with rate limiting.
*
* @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')
* @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.
* @return array The search response with 'results', 'total', 'paging' keys
* @throws RateLimitException When rate limit is hit
* @throws HubspotException On API errors
*/
public function search(string $objectType, array $payload): array
{
$endpoint = self::BASE_URL . "/crm/v3/objects/{$objectType}/search";
return $this->executeRequest(function () use ($endpoint, $payload) {
$response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
return $response->toArray();
});
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$deal = $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') {
return $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
return $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
}
/**
* @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 (RateLimitException $e) {
// throw $e;
} 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);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
17922
|
NULL
|
NULL
|
NULL
|
|
17925
|
780
|
41
|
2026-05-11T10:41:45.479493+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778496105479_m1.jpg...
|
PhpStorm
|
faVsco.js – Client.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
3
68
3
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\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 Illuminate\Support\Facades\Redis;
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)
{
$cacheKey = $this->getRateLimitCacheKey();
$cachedRetryAfter = Redis::get($cacheKey);
if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {
throw new RateLimitException(
'Hubspot rate limit (cached circuit-breaker)',
(int) $cachedRetryAfter,
);
}
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'policy' => $this->parsePolicy($e),
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function getRateLimitCacheKey(): string
{
return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());
}
public function isHubspotRateLimit(Throwable $e): bool
{
if ($e instanceof BadRequest
|| $e instanceof DealApiException
|| $e instanceof ContactApiException
|| $e instanceof CompanyApiException
|| $e instanceof \GuzzleHttp\Exception\RequestException
) {
return (int) $e->getCode() === 429;
}
return false;
}
public function parseRetryAfter(Throwable $e): int
{
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$e ' . PHP_EOL . print_r($e, true));
return 5;
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;
}
}
$policy = $this->parsePolicy($e);
if ($policy === 'TEN_SECONDLY_ROLLING') {
return 10;
}
if ($policy === 'SECONDLY') {
return 1;
}
if ($policy === 'DAILY_LIMIT') {
return 600;
}
$this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [
'exception_class' => get_class($e),
]);
return 10;
}
public function parsePolicy(Throwable $e): ?string
{
if (! method_exists($e, 'getResponseBody')) {
return null;
}
$body = $e->getResponseBody();
if (is_string($body)) {
$body = json_decode($body, true) ?? [];
}
if (! is_array($body)) {
return null;
}
$policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;
return is_string($policy) ? strtoupper($policy) : null;
}
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
);
}
/**
* Execute a search request against HubSpot CRM objects with rate limiting.
*
* @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')
* @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.
* @return array The search response with 'results', 'total', 'paging' keys
* @throws RateLimitException When rate limit is hit
* @throws HubspotException On API errors
*/
public function search(string $objectType, array $payload): array
{
$endpoint = self::BASE_URL . "/crm/v3/objects/{$objectType}/search";
return $this->executeRequest(function () use ($endpoint, $payload) {
$response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
return $response->toArray();
});
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$deal = $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') {
return $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
return $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
}
/**
* @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 (RateLimitException $e) {
// throw $e;
} 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);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
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":"JY-20725-handle-HS-search-rate-limit, menu","depth":5,"on_screen":true,"help_text":"Git Branch: JY-20725-handle-HS-search-rate-limit","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":"HandleHubspotRateLimitTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'HandleHubspotRateLimitTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'HandleHubspotRateLimitTest'","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":"AXStaticText","text":"3","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"68","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"3","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"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\\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 Illuminate\\Support\\Facades\\Redis;\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\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Reacts to a rate limits (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 $cacheKey = $this->getRateLimitCacheKey();\n\n $cachedRetryAfter = Redis::get($cacheKey);\n if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n (int) $cachedRetryAfter,\n );\n }\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);\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 'policy' => $this->parsePolicy($e),\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 getRateLimitCacheKey(): string\n {\n return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());\n }\n\n public function isHubspotRateLimit(Throwable $e): bool\n {\n if ($e instanceof BadRequest\n || $e instanceof DealApiException\n || $e instanceof ContactApiException\n || $e instanceof CompanyApiException\n || $e instanceof \\GuzzleHttp\\Exception\\RequestException\n ) {\n return (int) $e->getCode() === 429;\n }\n\n return false;\n }\n\n public function parseRetryAfter(Throwable $e): int\n {\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$e ' . PHP_EOL . print_r($e, true));\n return 5;\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 $policy = $this->parsePolicy($e);\n if ($policy === 'TEN_SECONDLY_ROLLING') {\n return 10;\n }\n if ($policy === 'SECONDLY') {\n return 1;\n }\n if ($policy === 'DAILY_LIMIT') {\n return 600;\n }\n\n $this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [\n 'exception_class' => get_class($e),\n ]);\n\n return 10;\n }\n\n public function parsePolicy(Throwable $e): ?string\n {\n if (! method_exists($e, 'getResponseBody')) {\n return null;\n }\n\n $body = $e->getResponseBody();\n if (is_string($body)) {\n $body = json_decode($body, true) ?? [];\n }\n\n if (! is_array($body)) {\n return null;\n }\n\n $policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;\n\n return is_string($policy) ? strtoupper($policy) : null;\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 * Execute a search request against HubSpot CRM objects with rate limiting.\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n * @return array The search response with 'results', 'total', 'paging' keys\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n */\n public function search(string $objectType, array $payload): array\n {\n $endpoint = self::BASE_URL . \"/crm/v3/objects/{$objectType}/search\";\n\n return $this->executeRequest(function () use ($endpoint, $payload) {\n $response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n return $response->toArray();\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 $deal = $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 return $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n return $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\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 (RateLimitException $e) {\n// throw $e;\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\\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 Illuminate\\Support\\Facades\\Redis;\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\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Reacts to a rate limits (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 $cacheKey = $this->getRateLimitCacheKey();\n\n $cachedRetryAfter = Redis::get($cacheKey);\n if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n (int) $cachedRetryAfter,\n );\n }\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);\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 'policy' => $this->parsePolicy($e),\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 getRateLimitCacheKey(): string\n {\n return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());\n }\n\n public function isHubspotRateLimit(Throwable $e): bool\n {\n if ($e instanceof BadRequest\n || $e instanceof DealApiException\n || $e instanceof ContactApiException\n || $e instanceof CompanyApiException\n || $e instanceof \\GuzzleHttp\\Exception\\RequestException\n ) {\n return (int) $e->getCode() === 429;\n }\n\n return false;\n }\n\n public function parseRetryAfter(Throwable $e): int\n {\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$e ' . PHP_EOL . print_r($e, true));\n return 5;\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 $policy = $this->parsePolicy($e);\n if ($policy === 'TEN_SECONDLY_ROLLING') {\n return 10;\n }\n if ($policy === 'SECONDLY') {\n return 1;\n }\n if ($policy === 'DAILY_LIMIT') {\n return 600;\n }\n\n $this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [\n 'exception_class' => get_class($e),\n ]);\n\n return 10;\n }\n\n public function parsePolicy(Throwable $e): ?string\n {\n if (! method_exists($e, 'getResponseBody')) {\n return null;\n }\n\n $body = $e->getResponseBody();\n if (is_string($body)) {\n $body = json_decode($body, true) ?? [];\n }\n\n if (! is_array($body)) {\n return null;\n }\n\n $policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;\n\n return is_string($policy) ? strtoupper($policy) : null;\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 * Execute a search request against HubSpot CRM objects with rate limiting.\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n * @return array The search response with 'results', 'total', 'paging' keys\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n */\n public function search(string $objectType, array $payload): array\n {\n $endpoint = self::BASE_URL . \"/crm/v3/objects/{$objectType}/search\";\n\n return $this->executeRequest(function () use ($endpoint, $payload) {\n $response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n return $response->toArray();\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 $deal = $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 return $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n return $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\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 (RateLimitException $e) {\n// throw $e;\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":"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":"19","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","depth":4,"on_screen":true,"value":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","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}]...
|
-6243931637791501581
|
6378618611371477092
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
3
68
3
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\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 Illuminate\Support\Facades\Redis;
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)
{
$cacheKey = $this->getRateLimitCacheKey();
$cachedRetryAfter = Redis::get($cacheKey);
if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {
throw new RateLimitException(
'Hubspot rate limit (cached circuit-breaker)',
(int) $cachedRetryAfter,
);
}
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'policy' => $this->parsePolicy($e),
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function getRateLimitCacheKey(): string
{
return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());
}
public function isHubspotRateLimit(Throwable $e): bool
{
if ($e instanceof BadRequest
|| $e instanceof DealApiException
|| $e instanceof ContactApiException
|| $e instanceof CompanyApiException
|| $e instanceof \GuzzleHttp\Exception\RequestException
) {
return (int) $e->getCode() === 429;
}
return false;
}
public function parseRetryAfter(Throwable $e): int
{
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$e ' . PHP_EOL . print_r($e, true));
return 5;
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;
}
}
$policy = $this->parsePolicy($e);
if ($policy === 'TEN_SECONDLY_ROLLING') {
return 10;
}
if ($policy === 'SECONDLY') {
return 1;
}
if ($policy === 'DAILY_LIMIT') {
return 600;
}
$this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [
'exception_class' => get_class($e),
]);
return 10;
}
public function parsePolicy(Throwable $e): ?string
{
if (! method_exists($e, 'getResponseBody')) {
return null;
}
$body = $e->getResponseBody();
if (is_string($body)) {
$body = json_decode($body, true) ?? [];
}
if (! is_array($body)) {
return null;
}
$policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;
return is_string($policy) ? strtoupper($policy) : null;
}
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
);
}
/**
* Execute a search request against HubSpot CRM objects with rate limiting.
*
* @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')
* @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.
* @return array The search response with 'results', 'total', 'paging' keys
* @throws RateLimitException When rate limit is hit
* @throws HubspotException On API errors
*/
public function search(string $objectType, array $payload): array
{
$endpoint = self::BASE_URL . "/crm/v3/objects/{$objectType}/search";
return $this->executeRequest(function () use ($endpoint, $payload) {
$response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
return $response->toArray();
});
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$deal = $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') {
return $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
return $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
}
/**
* @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 (RateLimitException $e) {
// throw $e;
} 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);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
17923
|
NULL
|
NULL
|
NULL
|
|
17926
|
780
|
42
|
2026-05-11T10:41:59.237018+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778496119237_m1.jpg...
|
PhpStorm
|
faVsco.js – Client.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
3
68
3
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\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 Illuminate\Support\Facades\Redis;
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)
{
$cacheKey = $this->getRateLimitCacheKey();
$cachedRetryAfter = Redis::get($cacheKey);
if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {
throw new RateLimitException(
'Hubspot rate limit (cached circuit-breaker)',
(int) $cachedRetryAfter,
);
}
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'policy' => $this->parsePolicy($e),
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function getRateLimitCacheKey(): string
{
return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());
}
public function isHubspotRateLimit(Throwable $e): bool
{
if ($e instanceof BadRequest
|| $e instanceof DealApiException
|| $e instanceof ContactApiException
|| $e instanceof CompanyApiException
|| $e instanceof \GuzzleHttp\Exception\RequestException
) {
return (int) $e->getCode() === 429;
}
return false;
}
public function parseRetryAfter(Throwable $e): int
{
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$e ' . PHP_EOL . print_r($e, true));
return 5;
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;
}
}
$policy = $this->parsePolicy($e);
if ($policy === 'TEN_SECONDLY_ROLLING') {
return 10;
}
if ($policy === 'SECONDLY') {
return 1;
}
if ($policy === 'DAILY_LIMIT') {
return 600;
}
$this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [
'exception_class' => get_class($e),
]);
return 10;
}
public function parsePolicy(Throwable $e): ?string
{
if (! method_exists($e, 'getResponseBody')) {
return null;
}
$body = $e->getResponseBody();
if (is_string($body)) {
$body = json_decode($body, true) ?? [];
}
if (! is_array($body)) {
return null;
}
$policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;
return is_string($policy) ? strtoupper($policy) : null;
}
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
);
}
/**
* Execute a search request against HubSpot CRM objects with rate limiting.
*
* @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')
* @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.
* @return array The search response with 'results', 'total', 'paging' keys
* @throws RateLimitException When rate limit is hit
* @throws HubspotException On API errors
*/
public function search(string $objectType, array $payload): array
{
$endpoint = self::BASE_URL . "/crm/v3/objects/{$objectType}/search";
return $this->executeRequest(function () use ($endpoint, $payload) {
$response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
return $response->toArray();
});
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$deal = $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') {
return $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
return $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
}
/**
* @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 (RateLimitException $e) {
// throw $e;
} 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);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error...
|
[{"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":"JY-20725-handle-HS-search-rate-limit, menu","depth":5,"on_screen":true,"help_text":"Git Branch: JY-20725-handle-HS-search-rate-limit","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":"HandleHubspotRateLimitTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'HandleHubspotRateLimitTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'HandleHubspotRateLimitTest'","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":"AXStaticText","text":"3","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"68","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"3","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"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\\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 Illuminate\\Support\\Facades\\Redis;\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\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Reacts to a rate limits (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 $cacheKey = $this->getRateLimitCacheKey();\n\n $cachedRetryAfter = Redis::get($cacheKey);\n if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n (int) $cachedRetryAfter,\n );\n }\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);\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 'policy' => $this->parsePolicy($e),\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 getRateLimitCacheKey(): string\n {\n return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());\n }\n\n public function isHubspotRateLimit(Throwable $e): bool\n {\n if ($e instanceof BadRequest\n || $e instanceof DealApiException\n || $e instanceof ContactApiException\n || $e instanceof CompanyApiException\n || $e instanceof \\GuzzleHttp\\Exception\\RequestException\n ) {\n return (int) $e->getCode() === 429;\n }\n\n return false;\n }\n\n public function parseRetryAfter(Throwable $e): int\n {\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$e ' . PHP_EOL . print_r($e, true));\n return 5;\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 $policy = $this->parsePolicy($e);\n if ($policy === 'TEN_SECONDLY_ROLLING') {\n return 10;\n }\n if ($policy === 'SECONDLY') {\n return 1;\n }\n if ($policy === 'DAILY_LIMIT') {\n return 600;\n }\n\n $this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [\n 'exception_class' => get_class($e),\n ]);\n\n return 10;\n }\n\n public function parsePolicy(Throwable $e): ?string\n {\n if (! method_exists($e, 'getResponseBody')) {\n return null;\n }\n\n $body = $e->getResponseBody();\n if (is_string($body)) {\n $body = json_decode($body, true) ?? [];\n }\n\n if (! is_array($body)) {\n return null;\n }\n\n $policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;\n\n return is_string($policy) ? strtoupper($policy) : null;\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 * Execute a search request against HubSpot CRM objects with rate limiting.\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n * @return array The search response with 'results', 'total', 'paging' keys\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n */\n public function search(string $objectType, array $payload): array\n {\n $endpoint = self::BASE_URL . \"/crm/v3/objects/{$objectType}/search\";\n\n return $this->executeRequest(function () use ($endpoint, $payload) {\n $response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n return $response->toArray();\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 $deal = $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 return $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n return $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\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 (RateLimitException $e) {\n// throw $e;\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\\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 Illuminate\\Support\\Facades\\Redis;\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\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Reacts to a rate limits (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 $cacheKey = $this->getRateLimitCacheKey();\n\n $cachedRetryAfter = Redis::get($cacheKey);\n if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n (int) $cachedRetryAfter,\n );\n }\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);\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 'policy' => $this->parsePolicy($e),\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 getRateLimitCacheKey(): string\n {\n return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());\n }\n\n public function isHubspotRateLimit(Throwable $e): bool\n {\n if ($e instanceof BadRequest\n || $e instanceof DealApiException\n || $e instanceof ContactApiException\n || $e instanceof CompanyApiException\n || $e instanceof \\GuzzleHttp\\Exception\\RequestException\n ) {\n return (int) $e->getCode() === 429;\n }\n\n return false;\n }\n\n public function parseRetryAfter(Throwable $e): int\n {\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$e ' . PHP_EOL . print_r($e, true));\n return 5;\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 $policy = $this->parsePolicy($e);\n if ($policy === 'TEN_SECONDLY_ROLLING') {\n return 10;\n }\n if ($policy === 'SECONDLY') {\n return 1;\n }\n if ($policy === 'DAILY_LIMIT') {\n return 600;\n }\n\n $this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [\n 'exception_class' => get_class($e),\n ]);\n\n return 10;\n }\n\n public function parsePolicy(Throwable $e): ?string\n {\n if (! method_exists($e, 'getResponseBody')) {\n return null;\n }\n\n $body = $e->getResponseBody();\n if (is_string($body)) {\n $body = json_decode($body, true) ?? [];\n }\n\n if (! is_array($body)) {\n return null;\n }\n\n $policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;\n\n return is_string($policy) ? strtoupper($policy) : null;\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 * Execute a search request against HubSpot CRM objects with rate limiting.\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n * @return array The search response with 'results', 'total', 'paging' keys\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n */\n public function search(string $objectType, array $payload): array\n {\n $endpoint = self::BASE_URL . \"/crm/v3/objects/{$objectType}/search\";\n\n return $this->executeRequest(function () use ($endpoint, $payload) {\n $response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n return $response->toArray();\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 $deal = $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 return $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n return $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\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 (RateLimitException $e) {\n// throw $e;\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":"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":"19","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
5408622603018220958
|
5225697106764630116
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
3
68
3
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\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 Illuminate\Support\Facades\Redis;
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)
{
$cacheKey = $this->getRateLimitCacheKey();
$cachedRetryAfter = Redis::get($cacheKey);
if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {
throw new RateLimitException(
'Hubspot rate limit (cached circuit-breaker)',
(int) $cachedRetryAfter,
);
}
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'policy' => $this->parsePolicy($e),
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function getRateLimitCacheKey(): string
{
return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());
}
public function isHubspotRateLimit(Throwable $e): bool
{
if ($e instanceof BadRequest
|| $e instanceof DealApiException
|| $e instanceof ContactApiException
|| $e instanceof CompanyApiException
|| $e instanceof \GuzzleHttp\Exception\RequestException
) {
return (int) $e->getCode() === 429;
}
return false;
}
public function parseRetryAfter(Throwable $e): int
{
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$e ' . PHP_EOL . print_r($e, true));
return 5;
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;
}
}
$policy = $this->parsePolicy($e);
if ($policy === 'TEN_SECONDLY_ROLLING') {
return 10;
}
if ($policy === 'SECONDLY') {
return 1;
}
if ($policy === 'DAILY_LIMIT') {
return 600;
}
$this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [
'exception_class' => get_class($e),
]);
return 10;
}
public function parsePolicy(Throwable $e): ?string
{
if (! method_exists($e, 'getResponseBody')) {
return null;
}
$body = $e->getResponseBody();
if (is_string($body)) {
$body = json_decode($body, true) ?? [];
}
if (! is_array($body)) {
return null;
}
$policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;
return is_string($policy) ? strtoupper($policy) : null;
}
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
);
}
/**
* Execute a search request against HubSpot CRM objects with rate limiting.
*
* @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')
* @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.
* @return array The search response with 'results', 'total', 'paging' keys
* @throws RateLimitException When rate limit is hit
* @throws HubspotException On API errors
*/
public function search(string $objectType, array $payload): array
{
$endpoint = self::BASE_URL . "/crm/v3/objects/{$objectType}/search";
return $this->executeRequest(function () use ($endpoint, $payload) {
$response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
return $response->toArray();
});
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$deal = $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') {
return $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
return $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
}
/**
* @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 (RateLimitException $e) {
// throw $e;
} 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);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
17927
|
781
|
40
|
2026-05-11T10:41:59.243647+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778496119243_m2.jpg...
|
PhpStorm
|
faVsco.js – Client.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
3
68
3
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\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 Illuminate\Support\Facades\Redis;
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)
{
$cacheKey = $this->getRateLimitCacheKey();
$cachedRetryAfter = Redis::get($cacheKey);
if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {
throw new RateLimitException(
'Hubspot rate limit (cached circuit-breaker)',
(int) $cachedRetryAfter,
);
}
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'policy' => $this->parsePolicy($e),
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function getRateLimitCacheKey(): string
{
return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());
}
public function isHubspotRateLimit(Throwable $e): bool
{
if ($e instanceof BadRequest
|| $e instanceof DealApiException
|| $e instanceof ContactApiException
|| $e instanceof CompanyApiException
|| $e instanceof \GuzzleHttp\Exception\RequestException
) {
return (int) $e->getCode() === 429;
}
return false;
}
public function parseRetryAfter(Throwable $e): int
{
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$e ' . PHP_EOL . print_r($e, true));
return 5;
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;
}
}
$policy = $this->parsePolicy($e);
if ($policy === 'TEN_SECONDLY_ROLLING') {
return 10;
}
if ($policy === 'SECONDLY') {
return 1;
}
if ($policy === 'DAILY_LIMIT') {
return 600;
}
$this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [
'exception_class' => get_class($e),
]);
return 10;
}
public function parsePolicy(Throwable $e): ?string
{
if (! method_exists($e, 'getResponseBody')) {
return null;
}
$body = $e->getResponseBody();
if (is_string($body)) {
$body = json_decode($body, true) ?? [];
}
if (! is_array($body)) {
return null;
}
$policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;
return is_string($policy) ? strtoupper($policy) : null;
}
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
);
}
/**
* Execute a search request against HubSpot CRM objects with rate limiting.
*
* @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')
* @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.
* @return array The search response with 'results', 'total', 'paging' keys
* @throws RateLimitException When rate limit is hit
* @throws HubspotException On API errors
*/
public function search(string $objectType, array $payload): array
{
$endpoint = self::BASE_URL . "/crm/v3/objects/{$objectType}/search";
return $this->executeRequest(function () use ($endpoint, $payload) {
$response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
return $response->toArray();
});
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$deal = $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') {
return $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
return $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
}
/**
* @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 (RateLimitException $e) {
// throw $e;
} 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);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}...
|
[{"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":"JY-20725-handle-HS-search-rate-limit, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.09541223,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: JY-20725-handle-HS-search-rate-limit","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.82413566,"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":"HandleHubspotRateLimitTest","depth":6,"bounds":{"left":0.8394282,"top":0.019952115,"width":0.076130316,"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 'HandleHubspotRateLimitTest'","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 'HandleHubspotRateLimitTest'","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":"AXStaticText","text":"3","depth":4,"bounds":{"left":0.5515292,"top":0.15003991,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"68","depth":4,"bounds":{"left":0.56150264,"top":0.15003991,"width":0.010305851,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"3","depth":4,"bounds":{"left":0.5738032,"top":0.15003991,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.5834442,"top":0.14844373,"width":0.00731383,"height":0.018355945},"on_screen":true,"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.59075797,"top":0.14844373,"width":0.006981383,"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\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\\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 Illuminate\\Support\\Facades\\Redis;\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\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Reacts to a rate limits (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 $cacheKey = $this->getRateLimitCacheKey();\n\n $cachedRetryAfter = Redis::get($cacheKey);\n if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n (int) $cachedRetryAfter,\n );\n }\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);\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 'policy' => $this->parsePolicy($e),\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 getRateLimitCacheKey(): string\n {\n return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());\n }\n\n public function isHubspotRateLimit(Throwable $e): bool\n {\n if ($e instanceof BadRequest\n || $e instanceof DealApiException\n || $e instanceof ContactApiException\n || $e instanceof CompanyApiException\n || $e instanceof \\GuzzleHttp\\Exception\\RequestException\n ) {\n return (int) $e->getCode() === 429;\n }\n\n return false;\n }\n\n public function parseRetryAfter(Throwable $e): int\n {\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$e ' . PHP_EOL . print_r($e, true));\n return 5;\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 $policy = $this->parsePolicy($e);\n if ($policy === 'TEN_SECONDLY_ROLLING') {\n return 10;\n }\n if ($policy === 'SECONDLY') {\n return 1;\n }\n if ($policy === 'DAILY_LIMIT') {\n return 600;\n }\n\n $this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [\n 'exception_class' => get_class($e),\n ]);\n\n return 10;\n }\n\n public function parsePolicy(Throwable $e): ?string\n {\n if (! method_exists($e, 'getResponseBody')) {\n return null;\n }\n\n $body = $e->getResponseBody();\n if (is_string($body)) {\n $body = json_decode($body, true) ?? [];\n }\n\n if (! is_array($body)) {\n return null;\n }\n\n $policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;\n\n return is_string($policy) ? strtoupper($policy) : null;\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 * Execute a search request against HubSpot CRM objects with rate limiting.\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n * @return array The search response with 'results', 'total', 'paging' keys\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n */\n public function search(string $objectType, array $payload): array\n {\n $endpoint = self::BASE_URL . \"/crm/v3/objects/{$objectType}/search\";\n\n return $this->executeRequest(function () use ($endpoint, $payload) {\n $response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n return $response->toArray();\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 $deal = $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 return $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n return $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\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 (RateLimitException $e) {\n// throw $e;\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\\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 Illuminate\\Support\\Facades\\Redis;\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\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Reacts to a rate limits (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 $cacheKey = $this->getRateLimitCacheKey();\n\n $cachedRetryAfter = Redis::get($cacheKey);\n if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n (int) $cachedRetryAfter,\n );\n }\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);\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 'policy' => $this->parsePolicy($e),\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 getRateLimitCacheKey(): string\n {\n return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());\n }\n\n public function isHubspotRateLimit(Throwable $e): bool\n {\n if ($e instanceof BadRequest\n || $e instanceof DealApiException\n || $e instanceof ContactApiException\n || $e instanceof CompanyApiException\n || $e instanceof \\GuzzleHttp\\Exception\\RequestException\n ) {\n return (int) $e->getCode() === 429;\n }\n\n return false;\n }\n\n public function parseRetryAfter(Throwable $e): int\n {\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$e ' . PHP_EOL . print_r($e, true));\n return 5;\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 $policy = $this->parsePolicy($e);\n if ($policy === 'TEN_SECONDLY_ROLLING') {\n return 10;\n }\n if ($policy === 'SECONDLY') {\n return 1;\n }\n if ($policy === 'DAILY_LIMIT') {\n return 600;\n }\n\n $this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [\n 'exception_class' => get_class($e),\n ]);\n\n return 10;\n }\n\n public function parsePolicy(Throwable $e): ?string\n {\n if (! method_exists($e, 'getResponseBody')) {\n return null;\n }\n\n $body = $e->getResponseBody();\n if (is_string($body)) {\n $body = json_decode($body, true) ?? [];\n }\n\n if (! is_array($body)) {\n return null;\n }\n\n $policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;\n\n return is_string($policy) ? strtoupper($policy) : null;\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 * Execute a search request against HubSpot CRM objects with rate limiting.\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n * @return array The search response with 'results', 'total', 'paging' keys\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n */\n public function search(string $objectType, array $payload): array\n {\n $endpoint = self::BASE_URL . \"/crm/v3/objects/{$objectType}/search\";\n\n return $this->executeRequest(function () use ($endpoint, $payload) {\n $response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n return $response->toArray();\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 $deal = $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 return $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n return $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\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 (RateLimitException $e) {\n// throw $e;\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":"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":"19","depth":4,"bounds":{"left":0.96276593,"top":0.07581804,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.9740692,"top":0.074221864,"width":0.00731383,"height":0.018355945},"on_screen":true,"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.98138297,"top":0.074221864,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","depth":4,"bounds":{"left":0.61702126,"top":0.0726257,"width":0.38297874,"height":0.9066241},"on_screen":true,"value":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-1006337435740056067
|
6378618611371477092
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
3
68
3
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\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 Illuminate\Support\Facades\Redis;
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)
{
$cacheKey = $this->getRateLimitCacheKey();
$cachedRetryAfter = Redis::get($cacheKey);
if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {
throw new RateLimitException(
'Hubspot rate limit (cached circuit-breaker)',
(int) $cachedRetryAfter,
);
}
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'policy' => $this->parsePolicy($e),
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function getRateLimitCacheKey(): string
{
return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());
}
public function isHubspotRateLimit(Throwable $e): bool
{
if ($e instanceof BadRequest
|| $e instanceof DealApiException
|| $e instanceof ContactApiException
|| $e instanceof CompanyApiException
|| $e instanceof \GuzzleHttp\Exception\RequestException
) {
return (int) $e->getCode() === 429;
}
return false;
}
public function parseRetryAfter(Throwable $e): int
{
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$e ' . PHP_EOL . print_r($e, true));
return 5;
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;
}
}
$policy = $this->parsePolicy($e);
if ($policy === 'TEN_SECONDLY_ROLLING') {
return 10;
}
if ($policy === 'SECONDLY') {
return 1;
}
if ($policy === 'DAILY_LIMIT') {
return 600;
}
$this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [
'exception_class' => get_class($e),
]);
return 10;
}
public function parsePolicy(Throwable $e): ?string
{
if (! method_exists($e, 'getResponseBody')) {
return null;
}
$body = $e->getResponseBody();
if (is_string($body)) {
$body = json_decode($body, true) ?? [];
}
if (! is_array($body)) {
return null;
}
$policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;
return is_string($policy) ? strtoupper($policy) : null;
}
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
);
}
/**
* Execute a search request against HubSpot CRM objects with rate limiting.
*
* @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')
* @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.
* @return array The search response with 'results', 'total', 'paging' keys
* @throws RateLimitException When rate limit is hit
* @throws HubspotException On API errors
*/
public function search(string $objectType, array $payload): array
{
$endpoint = self::BASE_URL . "/crm/v3/objects/{$objectType}/search";
return $this->executeRequest(function () use ($endpoint, $payload) {
$response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
return $response->toArray();
});
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$deal = $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') {
return $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
return $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
}
/**
* @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 (RateLimitException $e) {
// throw $e;
} 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);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
17928
|
780
|
43
|
2026-05-11T10:42:00.759521+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778496120759_m1.jpg...
|
PhpStorm
|
faVsco.js – Client.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
3
68
3
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\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 Illuminate\Support\Facades\Redis;
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)
{
$cacheKey = $this->getRateLimitCacheKey();
$cachedRetryAfter = Redis::get($cacheKey);
if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {
throw new RateLimitException(
'Hubspot rate limit (cached circuit-breaker)',
(int) $cachedRetryAfter,
);
}
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'policy' => $this->parsePolicy($e),
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function getRateLimitCacheKey(): string
{
return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());
}
public function isHubspotRateLimit(Throwable $e): bool
{
if ($e instanceof BadRequest
|| $e instanceof DealApiException
|| $e instanceof ContactApiException
|| $e instanceof CompanyApiException
|| $e instanceof \GuzzleHttp\Exception\RequestException
) {
return (int) $e->getCode() === 429;
}
return false;
}
public function parseRetryAfter(Throwable $e): int
{
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$e ' . PHP_EOL . print_r($e, true));
return 5;
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;
}
}
$policy = $this->parsePolicy($e);
if ($policy === 'TEN_SECONDLY_ROLLING') {
return 10;
}
if ($policy === 'SECONDLY') {
return 1;
}
if ($policy === 'DAILY_LIMIT') {
return 600;
}
$this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [
'exception_class' => get_class($e),
]);
return 10;
}
public function parsePolicy(Throwable $e): ?string
{
if (! method_exists($e, 'getResponseBody')) {
return null;
}
$body = $e->getResponseBody();
if (is_string($body)) {
$body = json_decode($body, true) ?? [];
}
if (! is_array($body)) {
return null;
}
$policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;
return is_string($policy) ? strtoupper($policy) : null;
}
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
);
}
/**
* Execute a search request against HubSpot CRM objects with rate limiting.
*
* @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')
* @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.
* @return array The search response with 'results', 'total', 'paging' keys
* @throws RateLimitException When rate limit is hit
* @throws HubspotException On API errors
*/
public function search(string $objectType, array $payload): array
{
$endpoint = self::BASE_URL . "/crm/v3/objects/{$objectType}/search";
return $this->executeRequest(function () use ($endpoint, $payload) {
$response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
return $response->toArray();
});
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$deal = $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') {
return $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
return $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
}
/**
* @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 (RateLimitException $e) {
// throw $e;
} 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);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
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":"JY-20725-handle-HS-search-rate-limit, menu","depth":5,"on_screen":true,"help_text":"Git Branch: JY-20725-handle-HS-search-rate-limit","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":"HandleHubspotRateLimitTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'HandleHubspotRateLimitTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'HandleHubspotRateLimitTest'","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":"AXStaticText","text":"3","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"68","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"3","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"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\\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 Illuminate\\Support\\Facades\\Redis;\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\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Reacts to a rate limits (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 $cacheKey = $this->getRateLimitCacheKey();\n\n $cachedRetryAfter = Redis::get($cacheKey);\n if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n (int) $cachedRetryAfter,\n );\n }\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);\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 'policy' => $this->parsePolicy($e),\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 getRateLimitCacheKey(): string\n {\n return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());\n }\n\n public function isHubspotRateLimit(Throwable $e): bool\n {\n if ($e instanceof BadRequest\n || $e instanceof DealApiException\n || $e instanceof ContactApiException\n || $e instanceof CompanyApiException\n || $e instanceof \\GuzzleHttp\\Exception\\RequestException\n ) {\n return (int) $e->getCode() === 429;\n }\n\n return false;\n }\n\n public function parseRetryAfter(Throwable $e): int\n {\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$e ' . PHP_EOL . print_r($e, true));\n return 5;\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 $policy = $this->parsePolicy($e);\n if ($policy === 'TEN_SECONDLY_ROLLING') {\n return 10;\n }\n if ($policy === 'SECONDLY') {\n return 1;\n }\n if ($policy === 'DAILY_LIMIT') {\n return 600;\n }\n\n $this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [\n 'exception_class' => get_class($e),\n ]);\n\n return 10;\n }\n\n public function parsePolicy(Throwable $e): ?string\n {\n if (! method_exists($e, 'getResponseBody')) {\n return null;\n }\n\n $body = $e->getResponseBody();\n if (is_string($body)) {\n $body = json_decode($body, true) ?? [];\n }\n\n if (! is_array($body)) {\n return null;\n }\n\n $policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;\n\n return is_string($policy) ? strtoupper($policy) : null;\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 * Execute a search request against HubSpot CRM objects with rate limiting.\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n * @return array The search response with 'results', 'total', 'paging' keys\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n */\n public function search(string $objectType, array $payload): array\n {\n $endpoint = self::BASE_URL . \"/crm/v3/objects/{$objectType}/search\";\n\n return $this->executeRequest(function () use ($endpoint, $payload) {\n $response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n return $response->toArray();\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 $deal = $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 return $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n return $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\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 (RateLimitException $e) {\n// throw $e;\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\\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 Illuminate\\Support\\Facades\\Redis;\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\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Reacts to a rate limits (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 $cacheKey = $this->getRateLimitCacheKey();\n\n $cachedRetryAfter = Redis::get($cacheKey);\n if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n (int) $cachedRetryAfter,\n );\n }\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);\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 'policy' => $this->parsePolicy($e),\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 getRateLimitCacheKey(): string\n {\n return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());\n }\n\n public function isHubspotRateLimit(Throwable $e): bool\n {\n if ($e instanceof BadRequest\n || $e instanceof DealApiException\n || $e instanceof ContactApiException\n || $e instanceof CompanyApiException\n || $e instanceof \\GuzzleHttp\\Exception\\RequestException\n ) {\n return (int) $e->getCode() === 429;\n }\n\n return false;\n }\n\n public function parseRetryAfter(Throwable $e): int\n {\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$e ' . PHP_EOL . print_r($e, true));\n return 5;\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 $policy = $this->parsePolicy($e);\n if ($policy === 'TEN_SECONDLY_ROLLING') {\n return 10;\n }\n if ($policy === 'SECONDLY') {\n return 1;\n }\n if ($policy === 'DAILY_LIMIT') {\n return 600;\n }\n\n $this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [\n 'exception_class' => get_class($e),\n ]);\n\n return 10;\n }\n\n public function parsePolicy(Throwable $e): ?string\n {\n if (! method_exists($e, 'getResponseBody')) {\n return null;\n }\n\n $body = $e->getResponseBody();\n if (is_string($body)) {\n $body = json_decode($body, true) ?? [];\n }\n\n if (! is_array($body)) {\n return null;\n }\n\n $policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;\n\n return is_string($policy) ? strtoupper($policy) : null;\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 * Execute a search request against HubSpot CRM objects with rate limiting.\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n * @return array The search response with 'results', 'total', 'paging' keys\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n */\n public function search(string $objectType, array $payload): array\n {\n $endpoint = self::BASE_URL . \"/crm/v3/objects/{$objectType}/search\";\n\n return $this->executeRequest(function () use ($endpoint, $payload) {\n $response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n return $response->toArray();\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 $deal = $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 return $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n return $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\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 (RateLimitException $e) {\n// throw $e;\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":"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":"19","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","depth":4,"on_screen":true,"value":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","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}]...
|
-6243931637791501581
|
6378618611371477092
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
3
68
3
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\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 Illuminate\Support\Facades\Redis;
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)
{
$cacheKey = $this->getRateLimitCacheKey();
$cachedRetryAfter = Redis::get($cacheKey);
if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {
throw new RateLimitException(
'Hubspot rate limit (cached circuit-breaker)',
(int) $cachedRetryAfter,
);
}
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'policy' => $this->parsePolicy($e),
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function getRateLimitCacheKey(): string
{
return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());
}
public function isHubspotRateLimit(Throwable $e): bool
{
if ($e instanceof BadRequest
|| $e instanceof DealApiException
|| $e instanceof ContactApiException
|| $e instanceof CompanyApiException
|| $e instanceof \GuzzleHttp\Exception\RequestException
) {
return (int) $e->getCode() === 429;
}
return false;
}
public function parseRetryAfter(Throwable $e): int
{
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$e ' . PHP_EOL . print_r($e, true));
return 5;
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;
}
}
$policy = $this->parsePolicy($e);
if ($policy === 'TEN_SECONDLY_ROLLING') {
return 10;
}
if ($policy === 'SECONDLY') {
return 1;
}
if ($policy === 'DAILY_LIMIT') {
return 600;
}
$this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [
'exception_class' => get_class($e),
]);
return 10;
}
public function parsePolicy(Throwable $e): ?string
{
if (! method_exists($e, 'getResponseBody')) {
return null;
}
$body = $e->getResponseBody();
if (is_string($body)) {
$body = json_decode($body, true) ?? [];
}
if (! is_array($body)) {
return null;
}
$policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;
return is_string($policy) ? strtoupper($policy) : null;
}
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
);
}
/**
* Execute a search request against HubSpot CRM objects with rate limiting.
*
* @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')
* @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.
* @return array The search response with 'results', 'total', 'paging' keys
* @throws RateLimitException When rate limit is hit
* @throws HubspotException On API errors
*/
public function search(string $objectType, array $payload): array
{
$endpoint = self::BASE_URL . "/crm/v3/objects/{$objectType}/search";
return $this->executeRequest(function () use ($endpoint, $payload) {
$response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
return $response->toArray();
});
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$deal = $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') {
return $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
return $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
}
/**
* @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 (RateLimitException $e) {
// throw $e;
} 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);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
17926
|
NULL
|
NULL
|
NULL
|
|
17929
|
781
|
41
|
2026-05-11T10:42:00.787768+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778496120787_m2.jpg...
|
PhpStorm
|
faVsco.js – Client.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
3
68
3
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\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 Illuminate\Support\Facades\Redis;
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)
{
$cacheKey = $this->getRateLimitCacheKey();
$cachedRetryAfter = Redis::get($cacheKey);
if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {
throw new RateLimitException(
'Hubspot rate limit (cached circuit-breaker)',
(int) $cachedRetryAfter,
);
}
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'policy' => $this->parsePolicy($e),
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function getRateLimitCacheKey(): string
{
return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());
}
public function isHubspotRateLimit(Throwable $e): bool
{
if ($e instanceof BadRequest
|| $e instanceof DealApiException
|| $e instanceof ContactApiException
|| $e instanceof CompanyApiException
|| $e instanceof \GuzzleHttp\Exception\RequestException
) {
return (int) $e->getCode() === 429;
}
return false;
}
public function parseRetryAfter(Throwable $e): int
{
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$e ' . PHP_EOL . print_r($e, true));
return 5;
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;
}
}
$policy = $this->parsePolicy($e);
if ($policy === 'TEN_SECONDLY_ROLLING') {
return 10;
}
if ($policy === 'SECONDLY') {
return 1;
}
if ($policy === 'DAILY_LIMIT') {
return 600;
}
$this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [
'exception_class' => get_class($e),
]);
return 10;
}
public function parsePolicy(Throwable $e): ?string
{
if (! method_exists($e, 'getResponseBody')) {
return null;
}
$body = $e->getResponseBody();
if (is_string($body)) {
$body = json_decode($body, true) ?? [];
}
if (! is_array($body)) {
return null;
}
$policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;
return is_string($policy) ? strtoupper($policy) : null;
}
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
);
}
/**
* Execute a search request against HubSpot CRM objects with rate limiting.
*
* @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')
* @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.
* @return array The search response with 'results', 'total', 'paging' keys
* @throws RateLimitException When rate limit is hit
* @throws HubspotException On API errors
*/
public function search(string $objectType, array $payload): array
{
$endpoint = self::BASE_URL . "/crm/v3/objects/{$objectType}/search";
return $this->executeRequest(function () use ($endpoint, $payload) {
$response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
return $response->toArray();
});
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$deal = $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') {
return $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
return $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
}
/**
* @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 (RateLimitException $e) {
// throw $e;
} 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);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
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":"JY-20725-handle-HS-search-rate-limit, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.09541223,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: JY-20725-handle-HS-search-rate-limit","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.82413566,"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":"HandleHubspotRateLimitTest","depth":6,"bounds":{"left":0.8394282,"top":0.019952115,"width":0.076130316,"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 'HandleHubspotRateLimitTest'","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 'HandleHubspotRateLimitTest'","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":"AXStaticText","text":"3","depth":4,"bounds":{"left":0.5515292,"top":0.15003991,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"68","depth":4,"bounds":{"left":0.56150264,"top":0.15003991,"width":0.010305851,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"3","depth":4,"bounds":{"left":0.5738032,"top":0.15003991,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.5834442,"top":0.14844373,"width":0.00731383,"height":0.018355945},"on_screen":true,"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.59075797,"top":0.14844373,"width":0.006981383,"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\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\\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 Illuminate\\Support\\Facades\\Redis;\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\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Reacts to a rate limits (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 $cacheKey = $this->getRateLimitCacheKey();\n\n $cachedRetryAfter = Redis::get($cacheKey);\n if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n (int) $cachedRetryAfter,\n );\n }\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);\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 'policy' => $this->parsePolicy($e),\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 getRateLimitCacheKey(): string\n {\n return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());\n }\n\n public function isHubspotRateLimit(Throwable $e): bool\n {\n if ($e instanceof BadRequest\n || $e instanceof DealApiException\n || $e instanceof ContactApiException\n || $e instanceof CompanyApiException\n || $e instanceof \\GuzzleHttp\\Exception\\RequestException\n ) {\n return (int) $e->getCode() === 429;\n }\n\n return false;\n }\n\n public function parseRetryAfter(Throwable $e): int\n {\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$e ' . PHP_EOL . print_r($e, true));\n return 5;\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 $policy = $this->parsePolicy($e);\n if ($policy === 'TEN_SECONDLY_ROLLING') {\n return 10;\n }\n if ($policy === 'SECONDLY') {\n return 1;\n }\n if ($policy === 'DAILY_LIMIT') {\n return 600;\n }\n\n $this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [\n 'exception_class' => get_class($e),\n ]);\n\n return 10;\n }\n\n public function parsePolicy(Throwable $e): ?string\n {\n if (! method_exists($e, 'getResponseBody')) {\n return null;\n }\n\n $body = $e->getResponseBody();\n if (is_string($body)) {\n $body = json_decode($body, true) ?? [];\n }\n\n if (! is_array($body)) {\n return null;\n }\n\n $policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;\n\n return is_string($policy) ? strtoupper($policy) : null;\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 * Execute a search request against HubSpot CRM objects with rate limiting.\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n * @return array The search response with 'results', 'total', 'paging' keys\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n */\n public function search(string $objectType, array $payload): array\n {\n $endpoint = self::BASE_URL . \"/crm/v3/objects/{$objectType}/search\";\n\n return $this->executeRequest(function () use ($endpoint, $payload) {\n $response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n return $response->toArray();\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 $deal = $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 return $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n return $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\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 (RateLimitException $e) {\n// throw $e;\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\\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 Illuminate\\Support\\Facades\\Redis;\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\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Reacts to a rate limits (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 $cacheKey = $this->getRateLimitCacheKey();\n\n $cachedRetryAfter = Redis::get($cacheKey);\n if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n (int) $cachedRetryAfter,\n );\n }\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);\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 'policy' => $this->parsePolicy($e),\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 getRateLimitCacheKey(): string\n {\n return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());\n }\n\n public function isHubspotRateLimit(Throwable $e): bool\n {\n if ($e instanceof BadRequest\n || $e instanceof DealApiException\n || $e instanceof ContactApiException\n || $e instanceof CompanyApiException\n || $e instanceof \\GuzzleHttp\\Exception\\RequestException\n ) {\n return (int) $e->getCode() === 429;\n }\n\n return false;\n }\n\n public function parseRetryAfter(Throwable $e): int\n {\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$e ' . PHP_EOL . print_r($e, true));\n return 5;\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 $policy = $this->parsePolicy($e);\n if ($policy === 'TEN_SECONDLY_ROLLING') {\n return 10;\n }\n if ($policy === 'SECONDLY') {\n return 1;\n }\n if ($policy === 'DAILY_LIMIT') {\n return 600;\n }\n\n $this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [\n 'exception_class' => get_class($e),\n ]);\n\n return 10;\n }\n\n public function parsePolicy(Throwable $e): ?string\n {\n if (! method_exists($e, 'getResponseBody')) {\n return null;\n }\n\n $body = $e->getResponseBody();\n if (is_string($body)) {\n $body = json_decode($body, true) ?? [];\n }\n\n if (! is_array($body)) {\n return null;\n }\n\n $policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;\n\n return is_string($policy) ? strtoupper($policy) : null;\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 * Execute a search request against HubSpot CRM objects with rate limiting.\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n * @return array The search response with 'results', 'total', 'paging' keys\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n */\n public function search(string $objectType, array $payload): array\n {\n $endpoint = self::BASE_URL . \"/crm/v3/objects/{$objectType}/search\";\n\n return $this->executeRequest(function () use ($endpoint, $payload) {\n $response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n return $response->toArray();\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 $deal = $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 return $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n return $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\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 (RateLimitException $e) {\n// throw $e;\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":"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":"19","depth":4,"bounds":{"left":0.96276593,"top":0.07581804,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.9740692,"top":0.074221864,"width":0.00731383,"height":0.018355945},"on_screen":true,"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.98138297,"top":0.074221864,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","depth":4,"bounds":{"left":0.61702126,"top":0.0726257,"width":0.38297874,"height":0.9066241},"on_screen":true,"lines":[{"char_start":273,"char_count":32,"bounds":{"left":0.61702126,"top":0.0,"width":0.080119684,"height":0.014365523}},{"char_start":305,"char_count":79,"bounds":{"left":0.61702126,"top":0.0,"width":0.20212767,"height":0.014365523}},{"char_start":384,"char_count":18,"bounds":{"left":0.61702126,"top":0.0,"width":0.043882977,"height":0.014365523}},{"char_start":402,"char_count":21,"bounds":{"left":0.61702126,"top":0.0,"width":0.051861703,"height":0.014365523}},{"char_start":423,"char_count":48,"bounds":{"left":0.61702126,"top":0.0,"width":0.12167553,"height":0.014365523}},{"char_start":471,"char_count":72,"bounds":{"left":0.61702126,"top":0.0015961692,"width":0.18384309,"height":0.014365523}},{"char_start":543,"char_count":40,"bounds":{"left":0.61702126,"top":0.01915403,"width":0.10106383,"height":0.014365523}},{"char_start":583,"char_count":41,"bounds":{"left":0.61702126,"top":0.03671189,"width":0.10372341,"height":0.014365523}},{"char_start":624,"char_count":72,"bounds":{"left":0.61702126,"top":0.054269753,"width":0.18384309,"height":0.014365523}},{"char_start":696,"char_count":219,"bounds":{"left":0.61702126,"top":0.07182761,"width":0.38297874,"height":0.014365523}},{"char_start":915,"char_count":83,"bounds":{"left":0.61702126,"top":0.08938547,"width":0.21243352,"height":0.014365523}},{"char_start":998,"char_count":20,"bounds":{"left":0.61702126,"top":0.10694334,"width":0.04920213,"height":0.014365523}},{"char_start":1018,"char_count":17,"bounds":{"left":0.61702126,"top":0.1245012,"width":0.041223403,"height":0.014365523}},{"char_start":1035,"char_count":203,"bounds":{"left":0.61702126,"top":0.14205906,"width":0.38297874,"height":0.014365523}},{"char_start":1238,"char_count":22,"bounds":{"left":0.61702126,"top":0.15961692,"width":0.05418883,"height":0.014365523}},{"char_start":1260,"char_count":23,"bounds":{"left":0.61702126,"top":0.17717478,"width":0.056848403,"height":0.014365523}},{"char_start":1283,"char_count":10,"bounds":{"left":0.61702126,"top":0.19473264,"width":0.023271276,"height":0.014365523}},{"char_start":1293,"char_count":27,"bounds":{"left":0.61702126,"top":0.2122905,"width":0.06715426,"height":0.014365523}},{"char_start":1320,"char_count":26,"bounds":{"left":0.61702126,"top":0.22984837,"width":0.06482713,"height":0.014365523}},{"char_start":1346,"char_count":23,"bounds":{"left":0.61702126,"top":0.24740623,"width":0.056848403,"height":0.014365523}},{"char_start":1369,"char_count":28,"bounds":{"left":0.61702126,"top":0.26496407,"width":0.06981383,"height":0.014365523}},{"char_start":1397,"char_count":57,"bounds":{"left":0.61702126,"top":0.28252193,"width":0.14494681,"height":0.014365523}}],"value":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","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}]...
|
-6243931637791501581
|
6378618611371477092
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
3
68
3
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\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 Illuminate\Support\Facades\Redis;
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)
{
$cacheKey = $this->getRateLimitCacheKey();
$cachedRetryAfter = Redis::get($cacheKey);
if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {
throw new RateLimitException(
'Hubspot rate limit (cached circuit-breaker)',
(int) $cachedRetryAfter,
);
}
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'policy' => $this->parsePolicy($e),
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function getRateLimitCacheKey(): string
{
return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());
}
public function isHubspotRateLimit(Throwable $e): bool
{
if ($e instanceof BadRequest
|| $e instanceof DealApiException
|| $e instanceof ContactApiException
|| $e instanceof CompanyApiException
|| $e instanceof \GuzzleHttp\Exception\RequestException
) {
return (int) $e->getCode() === 429;
}
return false;
}
public function parseRetryAfter(Throwable $e): int
{
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$e ' . PHP_EOL . print_r($e, true));
return 5;
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;
}
}
$policy = $this->parsePolicy($e);
if ($policy === 'TEN_SECONDLY_ROLLING') {
return 10;
}
if ($policy === 'SECONDLY') {
return 1;
}
if ($policy === 'DAILY_LIMIT') {
return 600;
}
$this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [
'exception_class' => get_class($e),
]);
return 10;
}
public function parsePolicy(Throwable $e): ?string
{
if (! method_exists($e, 'getResponseBody')) {
return null;
}
$body = $e->getResponseBody();
if (is_string($body)) {
$body = json_decode($body, true) ?? [];
}
if (! is_array($body)) {
return null;
}
$policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;
return is_string($policy) ? strtoupper($policy) : null;
}
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
);
}
/**
* Execute a search request against HubSpot CRM objects with rate limiting.
*
* @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')
* @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.
* @return array The search response with 'results', 'total', 'paging' keys
* @throws RateLimitException When rate limit is hit
* @throws HubspotException On API errors
*/
public function search(string $objectType, array $payload): array
{
$endpoint = self::BASE_URL . "/crm/v3/objects/{$objectType}/search";
return $this->executeRequest(function () use ($endpoint, $payload) {
$response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
return $response->toArray();
});
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$deal = $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') {
return $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
return $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
}
/**
* @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 (RateLimitException $e) {
// throw $e;
} 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);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
17927
|
NULL
|
NULL
|
NULL
|
|
17930
|
780
|
44
|
2026-05-11T10:42:03.652426+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778496123652_m1.jpg...
|
PhpStorm
|
faVsco.js – Client.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
3
68
3
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\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 Illuminate\Support\Facades\Redis;
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)
{
$cacheKey = $this->getRateLimitCacheKey();
$cachedRetryAfter = Redis::get($cacheKey);
if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {
throw new RateLimitException(
'Hubspot rate limit (cached circuit-breaker)',
(int) $cachedRetryAfter,
);
}
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'policy' => $this->parsePolicy($e),
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function getRateLimitCacheKey(): string
{
return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());
}
public function isHubspotRateLimit(Throwable $e): bool
{
if ($e instanceof BadRequest
|| $e instanceof DealApiException
|| $e instanceof ContactApiException
|| $e instanceof CompanyApiException
|| $e instanceof \GuzzleHttp\Exception\RequestException
) {
return (int) $e->getCode() === 429;
}
return false;
}
public function parseRetryAfter(Throwable $e): int
{
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$e ' . PHP_EOL . print_r($e, true));
return 5;
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;
}
}
$policy = $this->parsePolicy($e);
if ($policy === 'TEN_SECONDLY_ROLLING') {
return 10;
}
if ($policy === 'SECONDLY') {
return 1;
}
if ($policy === 'DAILY_LIMIT') {
return 600;
}
$this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [
'exception_class' => get_class($e),
]);
return 10;
}
public function parsePolicy(Throwable $e): ?string
{
if (! method_exists($e, 'getResponseBody')) {
return null;
}
$body = $e->getResponseBody();
if (is_string($body)) {
$body = json_decode($body, true) ?? [];
}
if (! is_array($body)) {
return null;
}
$policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;
return is_string($policy) ? strtoupper($policy) : null;
}
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
);
}
/**
* Execute a search request against HubSpot CRM objects with rate limiting.
*
* @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')
* @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.
* @return array The search response with 'results', 'total', 'paging' keys
* @throws RateLimitException When rate limit is hit
* @throws HubspotException On API errors
*/
public function search(string $objectType, array $payload): array
{
$endpoint = self::BASE_URL . "/crm/v3/objects/{$objectType}/search";
return $this->executeRequest(function () use ($endpoint, $payload) {
$response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
return $response->toArray();
});
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$deal = $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') {
return $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
return $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
}
/**
* @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 (RateLimitException $e) {
// throw $e;
} 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);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}...
|
[{"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":"JY-20725-handle-HS-search-rate-limit, menu","depth":5,"on_screen":true,"help_text":"Git Branch: JY-20725-handle-HS-search-rate-limit","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":"HandleHubspotRateLimitTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'HandleHubspotRateLimitTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'HandleHubspotRateLimitTest'","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":"AXStaticText","text":"3","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"68","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"3","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"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\\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 Illuminate\\Support\\Facades\\Redis;\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\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Reacts to a rate limits (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 $cacheKey = $this->getRateLimitCacheKey();\n\n $cachedRetryAfter = Redis::get($cacheKey);\n if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n (int) $cachedRetryAfter,\n );\n }\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);\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 'policy' => $this->parsePolicy($e),\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 getRateLimitCacheKey(): string\n {\n return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());\n }\n\n public function isHubspotRateLimit(Throwable $e): bool\n {\n if ($e instanceof BadRequest\n || $e instanceof DealApiException\n || $e instanceof ContactApiException\n || $e instanceof CompanyApiException\n || $e instanceof \\GuzzleHttp\\Exception\\RequestException\n ) {\n return (int) $e->getCode() === 429;\n }\n\n return false;\n }\n\n public function parseRetryAfter(Throwable $e): int\n {\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$e ' . PHP_EOL . print_r($e, true));\n return 5;\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 $policy = $this->parsePolicy($e);\n if ($policy === 'TEN_SECONDLY_ROLLING') {\n return 10;\n }\n if ($policy === 'SECONDLY') {\n return 1;\n }\n if ($policy === 'DAILY_LIMIT') {\n return 600;\n }\n\n $this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [\n 'exception_class' => get_class($e),\n ]);\n\n return 10;\n }\n\n public function parsePolicy(Throwable $e): ?string\n {\n if (! method_exists($e, 'getResponseBody')) {\n return null;\n }\n\n $body = $e->getResponseBody();\n if (is_string($body)) {\n $body = json_decode($body, true) ?? [];\n }\n\n if (! is_array($body)) {\n return null;\n }\n\n $policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;\n\n return is_string($policy) ? strtoupper($policy) : null;\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 * Execute a search request against HubSpot CRM objects with rate limiting.\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n * @return array The search response with 'results', 'total', 'paging' keys\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n */\n public function search(string $objectType, array $payload): array\n {\n $endpoint = self::BASE_URL . \"/crm/v3/objects/{$objectType}/search\";\n\n return $this->executeRequest(function () use ($endpoint, $payload) {\n $response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n return $response->toArray();\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 $deal = $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 return $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n return $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\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 (RateLimitException $e) {\n// throw $e;\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\\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 Illuminate\\Support\\Facades\\Redis;\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\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Reacts to a rate limits (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 $cacheKey = $this->getRateLimitCacheKey();\n\n $cachedRetryAfter = Redis::get($cacheKey);\n if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n (int) $cachedRetryAfter,\n );\n }\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);\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 'policy' => $this->parsePolicy($e),\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 getRateLimitCacheKey(): string\n {\n return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());\n }\n\n public function isHubspotRateLimit(Throwable $e): bool\n {\n if ($e instanceof BadRequest\n || $e instanceof DealApiException\n || $e instanceof ContactApiException\n || $e instanceof CompanyApiException\n || $e instanceof \\GuzzleHttp\\Exception\\RequestException\n ) {\n return (int) $e->getCode() === 429;\n }\n\n return false;\n }\n\n public function parseRetryAfter(Throwable $e): int\n {\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$e ' . PHP_EOL . print_r($e, true));\n return 5;\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 $policy = $this->parsePolicy($e);\n if ($policy === 'TEN_SECONDLY_ROLLING') {\n return 10;\n }\n if ($policy === 'SECONDLY') {\n return 1;\n }\n if ($policy === 'DAILY_LIMIT') {\n return 600;\n }\n\n $this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [\n 'exception_class' => get_class($e),\n ]);\n\n return 10;\n }\n\n public function parsePolicy(Throwable $e): ?string\n {\n if (! method_exists($e, 'getResponseBody')) {\n return null;\n }\n\n $body = $e->getResponseBody();\n if (is_string($body)) {\n $body = json_decode($body, true) ?? [];\n }\n\n if (! is_array($body)) {\n return null;\n }\n\n $policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;\n\n return is_string($policy) ? strtoupper($policy) : null;\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 * Execute a search request against HubSpot CRM objects with rate limiting.\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n * @return array The search response with 'results', 'total', 'paging' keys\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n */\n public function search(string $objectType, array $payload): array\n {\n $endpoint = self::BASE_URL . \"/crm/v3/objects/{$objectType}/search\";\n\n return $this->executeRequest(function () use ($endpoint, $payload) {\n $response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n return $response->toArray();\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 $deal = $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 return $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n return $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\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 (RateLimitException $e) {\n// throw $e;\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":"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":"19","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","depth":4,"on_screen":true,"value":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-1006337435740056067
|
6378618611371477092
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
3
68
3
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\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 Illuminate\Support\Facades\Redis;
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)
{
$cacheKey = $this->getRateLimitCacheKey();
$cachedRetryAfter = Redis::get($cacheKey);
if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {
throw new RateLimitException(
'Hubspot rate limit (cached circuit-breaker)',
(int) $cachedRetryAfter,
);
}
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'policy' => $this->parsePolicy($e),
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function getRateLimitCacheKey(): string
{
return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());
}
public function isHubspotRateLimit(Throwable $e): bool
{
if ($e instanceof BadRequest
|| $e instanceof DealApiException
|| $e instanceof ContactApiException
|| $e instanceof CompanyApiException
|| $e instanceof \GuzzleHttp\Exception\RequestException
) {
return (int) $e->getCode() === 429;
}
return false;
}
public function parseRetryAfter(Throwable $e): int
{
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$e ' . PHP_EOL . print_r($e, true));
return 5;
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;
}
}
$policy = $this->parsePolicy($e);
if ($policy === 'TEN_SECONDLY_ROLLING') {
return 10;
}
if ($policy === 'SECONDLY') {
return 1;
}
if ($policy === 'DAILY_LIMIT') {
return 600;
}
$this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [
'exception_class' => get_class($e),
]);
return 10;
}
public function parsePolicy(Throwable $e): ?string
{
if (! method_exists($e, 'getResponseBody')) {
return null;
}
$body = $e->getResponseBody();
if (is_string($body)) {
$body = json_decode($body, true) ?? [];
}
if (! is_array($body)) {
return null;
}
$policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;
return is_string($policy) ? strtoupper($policy) : null;
}
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
);
}
/**
* Execute a search request against HubSpot CRM objects with rate limiting.
*
* @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')
* @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.
* @return array The search response with 'results', 'total', 'paging' keys
* @throws RateLimitException When rate limit is hit
* @throws HubspotException On API errors
*/
public function search(string $objectType, array $payload): array
{
$endpoint = self::BASE_URL . "/crm/v3/objects/{$objectType}/search";
return $this->executeRequest(function () use ($endpoint, $payload) {
$response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
return $response->toArray();
});
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$deal = $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') {
return $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
return $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
}
/**
* @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 (RateLimitException $e) {
// throw $e;
} 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);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
17931
|
781
|
42
|
2026-05-11T10:42:03.662423+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778496123662_m2.jpg...
|
PhpStorm
|
faVsco.js – Client.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
3
68
3
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\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 Illuminate\Support\Facades\Redis;
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)
{
$cacheKey = $this->getRateLimitCacheKey();
$cachedRetryAfter = Redis::get($cacheKey);
if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {
throw new RateLimitException(
'Hubspot rate limit (cached circuit-breaker)',
(int) $cachedRetryAfter,
);
}
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'policy' => $this->parsePolicy($e),
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function getRateLimitCacheKey(): string
{
return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());
}
public function isHubspotRateLimit(Throwable $e): bool
{
if ($e instanceof BadRequest
|| $e instanceof DealApiException
|| $e instanceof ContactApiException
|| $e instanceof CompanyApiException
|| $e instanceof \GuzzleHttp\Exception\RequestException
) {
return (int) $e->getCode() === 429;
}
return false;
}
public function parseRetryAfter(Throwable $e): int
{
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$e ' . PHP_EOL . print_r($e, true));
return 5;
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;
}
}
$policy = $this->parsePolicy($e);
if ($policy === 'TEN_SECONDLY_ROLLING') {
return 10;
}
if ($policy === 'SECONDLY') {
return 1;
}
if ($policy === 'DAILY_LIMIT') {
return 600;
}
$this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [
'exception_class' => get_class($e),
]);
return 10;
}
public function parsePolicy(Throwable $e): ?string
{
if (! method_exists($e, 'getResponseBody')) {
return null;
}
$body = $e->getResponseBody();
if (is_string($body)) {
$body = json_decode($body, true) ?? [];
}
if (! is_array($body)) {
return null;
}
$policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;
return is_string($policy) ? strtoupper($policy) : null;
}
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
);
}
/**
* Execute a search request against HubSpot CRM objects with rate limiting.
*
* @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')
* @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.
* @return array The search response with 'results', 'total', 'paging' keys
* @throws RateLimitException When rate limit is hit
* @throws HubspotException On API errors
*/
public function search(string $objectType, array $payload): array
{
$endpoint = self::BASE_URL . "/crm/v3/objects/{$objectType}/search";
return $this->executeRequest(function () use ($endpoint, $payload) {
$response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
return $response->toArray();
});
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$deal = $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') {
return $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
return $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
}
/**
* @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 (RateLimitException $e) {
// throw $e;
} 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);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}...
|
[{"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":"JY-20725-handle-HS-search-rate-limit, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.09541223,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: JY-20725-handle-HS-search-rate-limit","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.82413566,"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":"HandleHubspotRateLimitTest","depth":6,"bounds":{"left":0.8394282,"top":0.019952115,"width":0.076130316,"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 'HandleHubspotRateLimitTest'","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 'HandleHubspotRateLimitTest'","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":"AXStaticText","text":"3","depth":4,"bounds":{"left":0.5515292,"top":0.15003991,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"68","depth":4,"bounds":{"left":0.56150264,"top":0.15003991,"width":0.010305851,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"3","depth":4,"bounds":{"left":0.5738032,"top":0.15003991,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.5834442,"top":0.14844373,"width":0.00731383,"height":0.018355945},"on_screen":true,"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.59075797,"top":0.14844373,"width":0.006981383,"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\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\\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 Illuminate\\Support\\Facades\\Redis;\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\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Reacts to a rate limits (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 $cacheKey = $this->getRateLimitCacheKey();\n\n $cachedRetryAfter = Redis::get($cacheKey);\n if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n (int) $cachedRetryAfter,\n );\n }\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);\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 'policy' => $this->parsePolicy($e),\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 getRateLimitCacheKey(): string\n {\n return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());\n }\n\n public function isHubspotRateLimit(Throwable $e): bool\n {\n if ($e instanceof BadRequest\n || $e instanceof DealApiException\n || $e instanceof ContactApiException\n || $e instanceof CompanyApiException\n || $e instanceof \\GuzzleHttp\\Exception\\RequestException\n ) {\n return (int) $e->getCode() === 429;\n }\n\n return false;\n }\n\n public function parseRetryAfter(Throwable $e): int\n {\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$e ' . PHP_EOL . print_r($e, true));\n return 5;\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 $policy = $this->parsePolicy($e);\n if ($policy === 'TEN_SECONDLY_ROLLING') {\n return 10;\n }\n if ($policy === 'SECONDLY') {\n return 1;\n }\n if ($policy === 'DAILY_LIMIT') {\n return 600;\n }\n\n $this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [\n 'exception_class' => get_class($e),\n ]);\n\n return 10;\n }\n\n public function parsePolicy(Throwable $e): ?string\n {\n if (! method_exists($e, 'getResponseBody')) {\n return null;\n }\n\n $body = $e->getResponseBody();\n if (is_string($body)) {\n $body = json_decode($body, true) ?? [];\n }\n\n if (! is_array($body)) {\n return null;\n }\n\n $policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;\n\n return is_string($policy) ? strtoupper($policy) : null;\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 * Execute a search request against HubSpot CRM objects with rate limiting.\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n * @return array The search response with 'results', 'total', 'paging' keys\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n */\n public function search(string $objectType, array $payload): array\n {\n $endpoint = self::BASE_URL . \"/crm/v3/objects/{$objectType}/search\";\n\n return $this->executeRequest(function () use ($endpoint, $payload) {\n $response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n return $response->toArray();\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 $deal = $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 return $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n return $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\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 (RateLimitException $e) {\n// throw $e;\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\\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 Illuminate\\Support\\Facades\\Redis;\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\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Reacts to a rate limits (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 $cacheKey = $this->getRateLimitCacheKey();\n\n $cachedRetryAfter = Redis::get($cacheKey);\n if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n (int) $cachedRetryAfter,\n );\n }\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);\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 'policy' => $this->parsePolicy($e),\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 getRateLimitCacheKey(): string\n {\n return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());\n }\n\n public function isHubspotRateLimit(Throwable $e): bool\n {\n if ($e instanceof BadRequest\n || $e instanceof DealApiException\n || $e instanceof ContactApiException\n || $e instanceof CompanyApiException\n || $e instanceof \\GuzzleHttp\\Exception\\RequestException\n ) {\n return (int) $e->getCode() === 429;\n }\n\n return false;\n }\n\n public function parseRetryAfter(Throwable $e): int\n {\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$e ' . PHP_EOL . print_r($e, true));\n return 5;\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 $policy = $this->parsePolicy($e);\n if ($policy === 'TEN_SECONDLY_ROLLING') {\n return 10;\n }\n if ($policy === 'SECONDLY') {\n return 1;\n }\n if ($policy === 'DAILY_LIMIT') {\n return 600;\n }\n\n $this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [\n 'exception_class' => get_class($e),\n ]);\n\n return 10;\n }\n\n public function parsePolicy(Throwable $e): ?string\n {\n if (! method_exists($e, 'getResponseBody')) {\n return null;\n }\n\n $body = $e->getResponseBody();\n if (is_string($body)) {\n $body = json_decode($body, true) ?? [];\n }\n\n if (! is_array($body)) {\n return null;\n }\n\n $policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;\n\n return is_string($policy) ? strtoupper($policy) : null;\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 * Execute a search request against HubSpot CRM objects with rate limiting.\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n * @return array The search response with 'results', 'total', 'paging' keys\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n */\n public function search(string $objectType, array $payload): array\n {\n $endpoint = self::BASE_URL . \"/crm/v3/objects/{$objectType}/search\";\n\n return $this->executeRequest(function () use ($endpoint, $payload) {\n $response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n return $response->toArray();\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 $deal = $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 return $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n return $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\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 (RateLimitException $e) {\n// throw $e;\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":"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":"19","depth":4,"bounds":{"left":0.96276593,"top":0.07581804,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.9740692,"top":0.074221864,"width":0.00731383,"height":0.018355945},"on_screen":true,"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.98138297,"top":0.074221864,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","depth":4,"bounds":{"left":0.61702126,"top":0.0726257,"width":0.38297874,"height":0.9066241},"on_screen":true,"lines":[{"char_start":273,"char_count":32,"bounds":{"left":0.61702126,"top":0.0,"width":0.080119684,"height":0.014365523}},{"char_start":305,"char_count":79,"bounds":{"left":0.61702126,"top":0.0,"width":0.20212767,"height":0.014365523}},{"char_start":384,"char_count":18,"bounds":{"left":0.61702126,"top":0.0,"width":0.043882977,"height":0.014365523}},{"char_start":402,"char_count":21,"bounds":{"left":0.61702126,"top":0.0,"width":0.051861703,"height":0.014365523}},{"char_start":423,"char_count":48,"bounds":{"left":0.61702126,"top":0.0,"width":0.12167553,"height":0.014365523}},{"char_start":471,"char_count":72,"bounds":{"left":0.61702126,"top":0.0015961692,"width":0.18384309,"height":0.014365523}},{"char_start":543,"char_count":40,"bounds":{"left":0.61702126,"top":0.01915403,"width":0.10106383,"height":0.014365523}},{"char_start":583,"char_count":41,"bounds":{"left":0.61702126,"top":0.03671189,"width":0.10372341,"height":0.014365523}},{"char_start":624,"char_count":72,"bounds":{"left":0.61702126,"top":0.054269753,"width":0.18384309,"height":0.014365523}},{"char_start":696,"char_count":219,"bounds":{"left":0.61702126,"top":0.07182761,"width":0.38297874,"height":0.014365523}},{"char_start":915,"char_count":83,"bounds":{"left":0.61702126,"top":0.08938547,"width":0.21243352,"height":0.014365523}},{"char_start":998,"char_count":20,"bounds":{"left":0.61702126,"top":0.10694334,"width":0.04920213,"height":0.014365523}},{"char_start":1018,"char_count":17,"bounds":{"left":0.61702126,"top":0.1245012,"width":0.041223403,"height":0.014365523}},{"char_start":1035,"char_count":203,"bounds":{"left":0.61702126,"top":0.14205906,"width":0.38297874,"height":0.014365523}},{"char_start":1238,"char_count":22,"bounds":{"left":0.61702126,"top":0.15961692,"width":0.05418883,"height":0.014365523}},{"char_start":1260,"char_count":23,"bounds":{"left":0.61702126,"top":0.17717478,"width":0.056848403,"height":0.014365523}},{"char_start":1283,"char_count":10,"bounds":{"left":0.61702126,"top":0.19473264,"width":0.023271276,"height":0.014365523}},{"char_start":1293,"char_count":27,"bounds":{"left":0.61702126,"top":0.2122905,"width":0.06715426,"height":0.014365523}},{"char_start":1320,"char_count":26,"bounds":{"left":0.61702126,"top":0.22984837,"width":0.06482713,"height":0.014365523}},{"char_start":1346,"char_count":23,"bounds":{"left":0.61702126,"top":0.24740623,"width":0.056848403,"height":0.014365523}},{"char_start":1369,"char_count":28,"bounds":{"left":0.61702126,"top":0.26496407,"width":0.06981383,"height":0.014365523}},{"char_start":1397,"char_count":57,"bounds":{"left":0.61702126,"top":0.28252193,"width":0.14494681,"height":0.014365523}}],"value":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-1006337435740056067
|
6378618611371477092
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
3
68
3
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\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 Illuminate\Support\Facades\Redis;
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)
{
$cacheKey = $this->getRateLimitCacheKey();
$cachedRetryAfter = Redis::get($cacheKey);
if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {
throw new RateLimitException(
'Hubspot rate limit (cached circuit-breaker)',
(int) $cachedRetryAfter,
);
}
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'policy' => $this->parsePolicy($e),
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function getRateLimitCacheKey(): string
{
return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());
}
public function isHubspotRateLimit(Throwable $e): bool
{
if ($e instanceof BadRequest
|| $e instanceof DealApiException
|| $e instanceof ContactApiException
|| $e instanceof CompanyApiException
|| $e instanceof \GuzzleHttp\Exception\RequestException
) {
return (int) $e->getCode() === 429;
}
return false;
}
public function parseRetryAfter(Throwable $e): int
{
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$e ' . PHP_EOL . print_r($e, true));
return 5;
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;
}
}
$policy = $this->parsePolicy($e);
if ($policy === 'TEN_SECONDLY_ROLLING') {
return 10;
}
if ($policy === 'SECONDLY') {
return 1;
}
if ($policy === 'DAILY_LIMIT') {
return 600;
}
$this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [
'exception_class' => get_class($e),
]);
return 10;
}
public function parsePolicy(Throwable $e): ?string
{
if (! method_exists($e, 'getResponseBody')) {
return null;
}
$body = $e->getResponseBody();
if (is_string($body)) {
$body = json_decode($body, true) ?? [];
}
if (! is_array($body)) {
return null;
}
$policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;
return is_string($policy) ? strtoupper($policy) : null;
}
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
);
}
/**
* Execute a search request against HubSpot CRM objects with rate limiting.
*
* @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')
* @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.
* @return array The search response with 'results', 'total', 'paging' keys
* @throws RateLimitException When rate limit is hit
* @throws HubspotException On API errors
*/
public function search(string $objectType, array $payload): array
{
$endpoint = self::BASE_URL . "/crm/v3/objects/{$objectType}/search";
return $this->executeRequest(function () use ($endpoint, $payload) {
$response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
return $response->toArray();
});
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$deal = $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') {
return $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
return $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
}
/**
* @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 (RateLimitException $e) {
// throw $e;
} 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);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
17932
|
780
|
45
|
2026-05-11T10:42:05.083946+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778496125083_m1.jpg...
|
PhpStorm
|
faVsco.js – Client.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
3
68
3
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\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 Illuminate\Support\Facades\Redis;
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)
{
$cacheKey = $this->getRateLimitCacheKey();
$cachedRetryAfter = Redis::get($cacheKey);
if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {
throw new RateLimitException(
'Hubspot rate limit (cached circuit-breaker)',
(int) $cachedRetryAfter,
);
}
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'policy' => $this->parsePolicy($e),
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function getRateLimitCacheKey(): string
{
return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());
}
public function isHubspotRateLimit(Throwable $e): bool
{
if ($e instanceof BadRequest
|| $e instanceof DealApiException
|| $e instanceof ContactApiException
|| $e instanceof CompanyApiException
|| $e instanceof \GuzzleHttp\Exception\RequestException
) {
return (int) $e->getCode() === 429;
}
return false;
}
public function parseRetryAfter(Throwable $e): int
{
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$e ' . PHP_EOL . print_r($e, true));
return 5;
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;
}
}
$policy = $this->parsePolicy($e);
if ($policy === 'TEN_SECONDLY_ROLLING') {
return 10;
}
if ($policy === 'SECONDLY') {
return 1;
}
if ($policy === 'DAILY_LIMIT') {
return 600;
}
$this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [
'exception_class' => get_class($e),
]);
return 10;
}
public function parsePolicy(Throwable $e): ?string
{
if (! method_exists($e, 'getResponseBody')) {
return null;
}
$body = $e->getResponseBody();
if (is_string($body)) {
$body = json_decode($body, true) ?? [];
}
if (! is_array($body)) {
return null;
}
$policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;
return is_string($policy) ? strtoupper($policy) : null;
}
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
);
}
/**
* Execute a search request against HubSpot CRM objects with rate limiting.
*
* @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')
* @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.
* @return array The search response with 'results', 'total', 'paging' keys
* @throws RateLimitException When rate limit is hit
* @throws HubspotException On API errors
*/
public function search(string $objectType, array $payload): array
{
$endpoint = self::BASE_URL . "/crm/v3/objects/{$objectType}/search";
return $this->executeRequest(function () use ($endpoint, $payload) {
$response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
return $response->toArray();
});
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$deal = $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') {
return $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
return $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
}
/**
* @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 (RateLimitException $e) {
// throw $e;
} 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);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
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":"JY-20725-handle-HS-search-rate-limit, menu","depth":5,"on_screen":true,"help_text":"Git Branch: JY-20725-handle-HS-search-rate-limit","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":"HandleHubspotRateLimitTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'HandleHubspotRateLimitTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'HandleHubspotRateLimitTest'","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":"AXStaticText","text":"3","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"68","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"3","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"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\\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 Illuminate\\Support\\Facades\\Redis;\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\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Reacts to a rate limits (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 $cacheKey = $this->getRateLimitCacheKey();\n\n $cachedRetryAfter = Redis::get($cacheKey);\n if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n (int) $cachedRetryAfter,\n );\n }\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);\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 'policy' => $this->parsePolicy($e),\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 getRateLimitCacheKey(): string\n {\n return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());\n }\n\n public function isHubspotRateLimit(Throwable $e): bool\n {\n if ($e instanceof BadRequest\n || $e instanceof DealApiException\n || $e instanceof ContactApiException\n || $e instanceof CompanyApiException\n || $e instanceof \\GuzzleHttp\\Exception\\RequestException\n ) {\n return (int) $e->getCode() === 429;\n }\n\n return false;\n }\n\n public function parseRetryAfter(Throwable $e): int\n {\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$e ' . PHP_EOL . print_r($e, true));\n return 5;\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 $policy = $this->parsePolicy($e);\n if ($policy === 'TEN_SECONDLY_ROLLING') {\n return 10;\n }\n if ($policy === 'SECONDLY') {\n return 1;\n }\n if ($policy === 'DAILY_LIMIT') {\n return 600;\n }\n\n $this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [\n 'exception_class' => get_class($e),\n ]);\n\n return 10;\n }\n\n public function parsePolicy(Throwable $e): ?string\n {\n if (! method_exists($e, 'getResponseBody')) {\n return null;\n }\n\n $body = $e->getResponseBody();\n if (is_string($body)) {\n $body = json_decode($body, true) ?? [];\n }\n\n if (! is_array($body)) {\n return null;\n }\n\n $policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;\n\n return is_string($policy) ? strtoupper($policy) : null;\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 * Execute a search request against HubSpot CRM objects with rate limiting.\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n * @return array The search response with 'results', 'total', 'paging' keys\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n */\n public function search(string $objectType, array $payload): array\n {\n $endpoint = self::BASE_URL . \"/crm/v3/objects/{$objectType}/search\";\n\n return $this->executeRequest(function () use ($endpoint, $payload) {\n $response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n return $response->toArray();\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 $deal = $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 return $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n return $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\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 (RateLimitException $e) {\n// throw $e;\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\\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 Illuminate\\Support\\Facades\\Redis;\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\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Reacts to a rate limits (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 $cacheKey = $this->getRateLimitCacheKey();\n\n $cachedRetryAfter = Redis::get($cacheKey);\n if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n (int) $cachedRetryAfter,\n );\n }\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);\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 'policy' => $this->parsePolicy($e),\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 getRateLimitCacheKey(): string\n {\n return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());\n }\n\n public function isHubspotRateLimit(Throwable $e): bool\n {\n if ($e instanceof BadRequest\n || $e instanceof DealApiException\n || $e instanceof ContactApiException\n || $e instanceof CompanyApiException\n || $e instanceof \\GuzzleHttp\\Exception\\RequestException\n ) {\n return (int) $e->getCode() === 429;\n }\n\n return false;\n }\n\n public function parseRetryAfter(Throwable $e): int\n {\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$e ' . PHP_EOL . print_r($e, true));\n return 5;\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 $policy = $this->parsePolicy($e);\n if ($policy === 'TEN_SECONDLY_ROLLING') {\n return 10;\n }\n if ($policy === 'SECONDLY') {\n return 1;\n }\n if ($policy === 'DAILY_LIMIT') {\n return 600;\n }\n\n $this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [\n 'exception_class' => get_class($e),\n ]);\n\n return 10;\n }\n\n public function parsePolicy(Throwable $e): ?string\n {\n if (! method_exists($e, 'getResponseBody')) {\n return null;\n }\n\n $body = $e->getResponseBody();\n if (is_string($body)) {\n $body = json_decode($body, true) ?? [];\n }\n\n if (! is_array($body)) {\n return null;\n }\n\n $policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;\n\n return is_string($policy) ? strtoupper($policy) : null;\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 * Execute a search request against HubSpot CRM objects with rate limiting.\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n * @return array The search response with 'results', 'total', 'paging' keys\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n */\n public function search(string $objectType, array $payload): array\n {\n $endpoint = self::BASE_URL . \"/crm/v3/objects/{$objectType}/search\";\n\n return $this->executeRequest(function () use ($endpoint, $payload) {\n $response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n return $response->toArray();\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 $deal = $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 return $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n return $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\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 (RateLimitException $e) {\n// throw $e;\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":"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":"19","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","depth":4,"on_screen":true,"value":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","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}]...
|
-6243931637791501581
|
6378618611371477092
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
3
68
3
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\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 Illuminate\Support\Facades\Redis;
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)
{
$cacheKey = $this->getRateLimitCacheKey();
$cachedRetryAfter = Redis::get($cacheKey);
if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {
throw new RateLimitException(
'Hubspot rate limit (cached circuit-breaker)',
(int) $cachedRetryAfter,
);
}
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'policy' => $this->parsePolicy($e),
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function getRateLimitCacheKey(): string
{
return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());
}
public function isHubspotRateLimit(Throwable $e): bool
{
if ($e instanceof BadRequest
|| $e instanceof DealApiException
|| $e instanceof ContactApiException
|| $e instanceof CompanyApiException
|| $e instanceof \GuzzleHttp\Exception\RequestException
) {
return (int) $e->getCode() === 429;
}
return false;
}
public function parseRetryAfter(Throwable $e): int
{
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$e ' . PHP_EOL . print_r($e, true));
return 5;
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;
}
}
$policy = $this->parsePolicy($e);
if ($policy === 'TEN_SECONDLY_ROLLING') {
return 10;
}
if ($policy === 'SECONDLY') {
return 1;
}
if ($policy === 'DAILY_LIMIT') {
return 600;
}
$this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [
'exception_class' => get_class($e),
]);
return 10;
}
public function parsePolicy(Throwable $e): ?string
{
if (! method_exists($e, 'getResponseBody')) {
return null;
}
$body = $e->getResponseBody();
if (is_string($body)) {
$body = json_decode($body, true) ?? [];
}
if (! is_array($body)) {
return null;
}
$policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;
return is_string($policy) ? strtoupper($policy) : null;
}
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
);
}
/**
* Execute a search request against HubSpot CRM objects with rate limiting.
*
* @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')
* @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.
* @return array The search response with 'results', 'total', 'paging' keys
* @throws RateLimitException When rate limit is hit
* @throws HubspotException On API errors
*/
public function search(string $objectType, array $payload): array
{
$endpoint = self::BASE_URL . "/crm/v3/objects/{$objectType}/search";
return $this->executeRequest(function () use ($endpoint, $payload) {
$response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
return $response->toArray();
});
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$deal = $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') {
return $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
return $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
}
/**
* @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 (RateLimitException $e) {
// throw $e;
} 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);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
17930
|
NULL
|
NULL
|
NULL
|
|
17933
|
781
|
43
|
2026-05-11T10:42:09.292590+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778496129292_m2.jpg...
|
PhpStorm
|
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Inherited members (⌘R)
Anonymous Classes (⌘I)
Lamb Inherited members (⌘R)
Anonymous Classes (⌘I)
Lambdas (⌘L)
loading…
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":"loading…","depth":4,"bounds":{"left":0.5299202,"top":0.36472467,"width":0.023603724,"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"}]...
|
5797934796897005000
|
1410411586113665
|
visual_change
|
hybrid
|
NULL
|
Inherited members (⌘R)
Anonymous Classes (⌘I)
Lamb Inherited members (⌘R)
Anonymous Classes (⌘I)
Lambdas (⌘L)
loading…
Client.php
PhostormVIewINavicareCodeLaravelTOOISFV faVsco.js°9 JY-20725-handle-HS-search-rate-limroledeyC) TrackAutomated ReportGeneratedzventonp© TrackRecordingFileSiz© TrackRecordingSizeEnT. ValidateSmitProspect:T SyncCrmEntitiesTrait.php© CheckAndRetryRemoteMatch.phpAjReportsBadkequest.phpC) Kernel.php© PaginationState.phg0 Calendarn Conference1300 Crm@ bullnorn> OJ Close_copper>J Crmobiects_ DecorateAcuiviiy• Dummy143144145) Helpersv h Hubspot147AccountSvncStrate>D Actionsa ContactsvncStraterM Fields• M lournal1 Metadatalv OpportunitySyncSt>MConcerns.(c) Hubsnotl actMoC HubspotLastMo(C) Hubsnotl actMo© HubspotLastMo(C) Hubsnotl actMo© HubspotSingleS© HubspotSyncStr© HubspotWebhoo~ M Padination© HubspotPaginat© PaginationConfi(C) PaqinationState> D ProspectSearchStr:> D Redisv D ServiceTraitsTOnoortunitvsvnc() SvncCrmEntities176171T SuncFieldstirait.() WriteCrmTrait.n174• M UtilsM WebhookC) BatchSvncCollectot177(c) RatchSvncRedisSec) Client nho(C) ClocedDea|Stadecs@ DoalFieldsService rTacts naccod. 12 (20 minutes aao)class Cllent extends Baseclient 1mpLements Hubspotcllentintertacepublic function parseRetryAfter(Throwable $e): intifcis arrav(Svalue))Svalve = Svaluel0l ?> nult.if Gis numeric($value)) {return (int) $valve;Spolicy = $this->parsePolicy($e);if (Spolicy ==='TEN_SECONDLY_ROLLING') {notunn 10+if (Spolicy === 'SECONDLY') {return 1:if (Spolicy === 'DAILY LIMIT') {return 600Sthis->log->warning('[Hubspotl No retry-after header or policy name found. using default'. ['exception class' => qet class(Se).return 10:public function parsePolicy(Throwable $e): ?stringif d method existsSe.method:"aetResnonseBodv'))^netunn null.$body = $e->getResponseBodyO:if (is string($body)) {$body = ison_ decode($body,associative: true) ?? (]:if (! is array($body)) {Spolicy = Sbodví'policvName'] 22 Sbody[ 'policy'] 2> Sbodvl 'context'][ 'policyName'] 22 null:return is string(Spolicv) 2 strtoupper(Spolicy) : null:(c) HubSpot/Service.onpoveryaeuvitycrmlaskJoo.pnpA RateLimitException.ong© PaginationConfig.phpA3468 V3A1 MIM 1" suppont Dally • In 1h 10m100% LzP• Mon 11 May 13:42:08HandleHubspotRateLimitTest v« console (PROD]* console (EUl& console [STAGINGIw.19A= laravel.log4 SF [jiminny@localhost]& HS_local [jiminny@localhost][2026-05-07 14:21:15] local.INF0: [Hubspot] DEBUG Getting headers {"Date":["Thu,07 May 2026 14:21:15 GMT"],"Content-Type":["application/json;charset=utf-8"]."Tnancfen-Encoding": ["chunked"J,"conneccion". Keep-alive"n"CF-Ray" : ["9f80deb8db60dc3a-SOF"],"CF-Cache-Status":"DYNAMIC")"Strict-Transport-Secur1ty":"max-aqe=31536000* 1ncludeSubDomains: preload")accept-encoding"],access-control-allow-credentials": "false",server-timing": ["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",cfn:desc="9-80de8ercodcSa-TAD"'x-content-type-options": ["nosniff"]."x-hubsoot-correlation-id":"019e02d0-6fd8-7812-bdba-885b7ccb3ee3"]"Set-Cookie".[" cf bm=STUrtd0aXVrik50odaF6hZVYKhzTn0BidvMabeCtm0Y-1778163675-[IP_ADDRESS]-рT. ZaatDKxTae5zr8 2abBfWM00. ufZEXDZuHz2mBUFdzdo2aTHEs0)07-May-26 14:51:15 GMT; domain=.hubapi.com; Http0nly; Secure; SameSite=None"],"Renont-To".f","endnoints"•!\"url\":\"https:|V/\V/a.nel.cloudflare.com/\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZLzoYdxI%2BIxVpHmsKn30%2BKVA3mFIJ2m7YRECDGS\"group\":\"cf-nel\",\"max_age\":604800}"],"success_fraction\":0.01,"repоrт_со ."cт-nel\"\"max_age\":604800}"],"Server": ["cloudflare"]}} {"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab".id": "c7ab8365-903f-46d4-9403-0e5b551e3545"}W Windsurf Teams 76:28 UTF-8 P 4 spaces ®...
|
17931
|
NULL
|
NULL
|
NULL
|
|
17934
|
781
|
44
|
2026-05-11T10:42:12.295071+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778496132295_m2.jpg...
|
PhpStorm
|
faVsco.js – Client.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
3
68
3
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\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 Illuminate\Support\Facades\Redis;
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)
{
$cacheKey = $this->getRateLimitCacheKey();
$cachedRetryAfter = Redis::get($cacheKey);
if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {
throw new RateLimitException(
'Hubspot rate limit (cached circuit-breaker)',
(int) $cachedRetryAfter,
);
}
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'policy' => $this->parsePolicy($e),
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function getRateLimitCacheKey(): string
{
return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());
}
public function isHubspotRateLimit(Throwable $e): bool
{
if ($e instanceof BadRequest
|| $e instanceof DealApiException
|| $e instanceof ContactApiException
|| $e instanceof CompanyApiException
|| $e instanceof \GuzzleHttp\Exception\RequestException
) {
return (int) $e->getCode() === 429;
}
return false;
}
public function parseRetryAfter(Throwable $e): int
{
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$e ' . PHP_EOL . print_r($e, true));
return 5;
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;
}
}
$policy = $this->parsePolicy($e);
if ($policy === 'TEN_SECONDLY_ROLLING') {
return 10;
}
if ($policy === 'SECONDLY') {
return 1;
}
if ($policy === 'DAILY_LIMIT') {
return 600;
}
$this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [
'exception_class' => get_class($e),
]);
return 10;
}
public function parsePolicy(Throwable $e): ?string
{
if (! method_exists($e, 'getResponseBody')) {
return null;
}
$body = $e->getResponseBody();
if (is_string($body)) {
$body = json_decode($body, true) ?? [];
}
if (! is_array($body)) {
return null;
}
$policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;
return is_string($policy) ? strtoupper($policy) : null;
}
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
);
}
/**
* Execute a search request against HubSpot CRM objects with rate limiting.
*
* @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')
* @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.
* @return array The search response with 'results', 'total', 'paging' keys
* @throws RateLimitException When rate limit is hit
* @throws HubspotException On API errors
*/
public function search(string $objectType, array $payload): array
{
$endpoint = self::BASE_URL . "/crm/v3/objects/{$objectType}/search";
return $this->executeRequest(function () use ($endpoint, $payload) {
$response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
return $response->toArray();
});
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$deal = $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') {
return $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
return $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
}
/**
* @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 (RateLimitException $e) {
// throw $e;
} 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);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
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":"JY-20725-handle-HS-search-rate-limit, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.09541223,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: JY-20725-handle-HS-search-rate-limit","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.82413566,"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":"HandleHubspotRateLimitTest","depth":6,"bounds":{"left":0.8394282,"top":0.019952115,"width":0.076130316,"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 'HandleHubspotRateLimitTest'","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 'HandleHubspotRateLimitTest'","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":"AXStaticText","text":"3","depth":4,"bounds":{"left":0.5515292,"top":0.15003991,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"68","depth":4,"bounds":{"left":0.56150264,"top":0.15003991,"width":0.010305851,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"3","depth":4,"bounds":{"left":0.5738032,"top":0.15003991,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.5834442,"top":0.14844373,"width":0.00731383,"height":0.018355945},"on_screen":true,"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.59075797,"top":0.14844373,"width":0.006981383,"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\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\\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 Illuminate\\Support\\Facades\\Redis;\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\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Reacts to a rate limits (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 $cacheKey = $this->getRateLimitCacheKey();\n\n $cachedRetryAfter = Redis::get($cacheKey);\n if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n (int) $cachedRetryAfter,\n );\n }\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);\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 'policy' => $this->parsePolicy($e),\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 getRateLimitCacheKey(): string\n {\n return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());\n }\n\n public function isHubspotRateLimit(Throwable $e): bool\n {\n if ($e instanceof BadRequest\n || $e instanceof DealApiException\n || $e instanceof ContactApiException\n || $e instanceof CompanyApiException\n || $e instanceof \\GuzzleHttp\\Exception\\RequestException\n ) {\n return (int) $e->getCode() === 429;\n }\n\n return false;\n }\n\n public function parseRetryAfter(Throwable $e): int\n {\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$e ' . PHP_EOL . print_r($e, true));\n return 5;\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 $policy = $this->parsePolicy($e);\n if ($policy === 'TEN_SECONDLY_ROLLING') {\n return 10;\n }\n if ($policy === 'SECONDLY') {\n return 1;\n }\n if ($policy === 'DAILY_LIMIT') {\n return 600;\n }\n\n $this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [\n 'exception_class' => get_class($e),\n ]);\n\n return 10;\n }\n\n public function parsePolicy(Throwable $e): ?string\n {\n if (! method_exists($e, 'getResponseBody')) {\n return null;\n }\n\n $body = $e->getResponseBody();\n if (is_string($body)) {\n $body = json_decode($body, true) ?? [];\n }\n\n if (! is_array($body)) {\n return null;\n }\n\n $policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;\n\n return is_string($policy) ? strtoupper($policy) : null;\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 * Execute a search request against HubSpot CRM objects with rate limiting.\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n * @return array The search response with 'results', 'total', 'paging' keys\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n */\n public function search(string $objectType, array $payload): array\n {\n $endpoint = self::BASE_URL . \"/crm/v3/objects/{$objectType}/search\";\n\n return $this->executeRequest(function () use ($endpoint, $payload) {\n $response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n return $response->toArray();\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 $deal = $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 return $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n return $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\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 (RateLimitException $e) {\n// throw $e;\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\\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 Illuminate\\Support\\Facades\\Redis;\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\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Reacts to a rate limits (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 $cacheKey = $this->getRateLimitCacheKey();\n\n $cachedRetryAfter = Redis::get($cacheKey);\n if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n (int) $cachedRetryAfter,\n );\n }\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);\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 'policy' => $this->parsePolicy($e),\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 getRateLimitCacheKey(): string\n {\n return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());\n }\n\n public function isHubspotRateLimit(Throwable $e): bool\n {\n if ($e instanceof BadRequest\n || $e instanceof DealApiException\n || $e instanceof ContactApiException\n || $e instanceof CompanyApiException\n || $e instanceof \\GuzzleHttp\\Exception\\RequestException\n ) {\n return (int) $e->getCode() === 429;\n }\n\n return false;\n }\n\n public function parseRetryAfter(Throwable $e): int\n {\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$e ' . PHP_EOL . print_r($e, true));\n return 5;\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 $policy = $this->parsePolicy($e);\n if ($policy === 'TEN_SECONDLY_ROLLING') {\n return 10;\n }\n if ($policy === 'SECONDLY') {\n return 1;\n }\n if ($policy === 'DAILY_LIMIT') {\n return 600;\n }\n\n $this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [\n 'exception_class' => get_class($e),\n ]);\n\n return 10;\n }\n\n public function parsePolicy(Throwable $e): ?string\n {\n if (! method_exists($e, 'getResponseBody')) {\n return null;\n }\n\n $body = $e->getResponseBody();\n if (is_string($body)) {\n $body = json_decode($body, true) ?? [];\n }\n\n if (! is_array($body)) {\n return null;\n }\n\n $policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;\n\n return is_string($policy) ? strtoupper($policy) : null;\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 * Execute a search request against HubSpot CRM objects with rate limiting.\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n * @return array The search response with 'results', 'total', 'paging' keys\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n */\n public function search(string $objectType, array $payload): array\n {\n $endpoint = self::BASE_URL . \"/crm/v3/objects/{$objectType}/search\";\n\n return $this->executeRequest(function () use ($endpoint, $payload) {\n $response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n return $response->toArray();\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 $deal = $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 return $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n return $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\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 (RateLimitException $e) {\n// throw $e;\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":"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":"19","depth":4,"bounds":{"left":0.96276593,"top":0.07581804,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.9740692,"top":0.074221864,"width":0.00731383,"height":0.018355945},"on_screen":true,"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.98138297,"top":0.074221864,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","depth":4,"bounds":{"left":0.61702126,"top":0.0726257,"width":0.38297874,"height":0.9066241},"on_screen":true,"lines":[{"char_start":273,"char_count":32,"bounds":{"left":0.61702126,"top":0.0,"width":0.080119684,"height":0.014365523}},{"char_start":305,"char_count":79,"bounds":{"left":0.61702126,"top":0.0,"width":0.20212767,"height":0.014365523}},{"char_start":384,"char_count":18,"bounds":{"left":0.61702126,"top":0.0,"width":0.043882977,"height":0.014365523}},{"char_start":402,"char_count":21,"bounds":{"left":0.61702126,"top":0.0,"width":0.051861703,"height":0.014365523}},{"char_start":423,"char_count":48,"bounds":{"left":0.61702126,"top":0.0,"width":0.12167553,"height":0.014365523}},{"char_start":471,"char_count":72,"bounds":{"left":0.61702126,"top":0.0015961692,"width":0.18384309,"height":0.014365523}},{"char_start":543,"char_count":40,"bounds":{"left":0.61702126,"top":0.01915403,"width":0.10106383,"height":0.014365523}},{"char_start":583,"char_count":41,"bounds":{"left":0.61702126,"top":0.03671189,"width":0.10372341,"height":0.014365523}},{"char_start":624,"char_count":72,"bounds":{"left":0.61702126,"top":0.054269753,"width":0.18384309,"height":0.014365523}},{"char_start":696,"char_count":219,"bounds":{"left":0.61702126,"top":0.07182761,"width":0.38297874,"height":0.014365523}},{"char_start":915,"char_count":83,"bounds":{"left":0.61702126,"top":0.08938547,"width":0.21243352,"height":0.014365523}},{"char_start":998,"char_count":20,"bounds":{"left":0.61702126,"top":0.10694334,"width":0.04920213,"height":0.014365523}},{"char_start":1018,"char_count":17,"bounds":{"left":0.61702126,"top":0.1245012,"width":0.041223403,"height":0.014365523}},{"char_start":1035,"char_count":203,"bounds":{"left":0.61702126,"top":0.14205906,"width":0.38297874,"height":0.014365523}},{"char_start":1238,"char_count":22,"bounds":{"left":0.61702126,"top":0.15961692,"width":0.05418883,"height":0.014365523}},{"char_start":1260,"char_count":23,"bounds":{"left":0.61702126,"top":0.17717478,"width":0.056848403,"height":0.014365523}},{"char_start":1283,"char_count":10,"bounds":{"left":0.61702126,"top":0.19473264,"width":0.023271276,"height":0.014365523}},{"char_start":1293,"char_count":27,"bounds":{"left":0.61702126,"top":0.2122905,"width":0.06715426,"height":0.014365523}},{"char_start":1320,"char_count":26,"bounds":{"left":0.61702126,"top":0.22984837,"width":0.06482713,"height":0.014365523}},{"char_start":1346,"char_count":23,"bounds":{"left":0.61702126,"top":0.24740623,"width":0.056848403,"height":0.014365523}},{"char_start":1369,"char_count":28,"bounds":{"left":0.61702126,"top":0.26496407,"width":0.06981383,"height":0.014365523}},{"char_start":1397,"char_count":57,"bounds":{"left":0.61702126,"top":0.28252193,"width":0.14494681,"height":0.014365523}}],"value":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","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}]...
|
-6243931637791501581
|
6378618611371477092
|
visual_change
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
3
68
3
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\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 Illuminate\Support\Facades\Redis;
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)
{
$cacheKey = $this->getRateLimitCacheKey();
$cachedRetryAfter = Redis::get($cacheKey);
if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {
throw new RateLimitException(
'Hubspot rate limit (cached circuit-breaker)',
(int) $cachedRetryAfter,
);
}
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'policy' => $this->parsePolicy($e),
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function getRateLimitCacheKey(): string
{
return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());
}
public function isHubspotRateLimit(Throwable $e): bool
{
if ($e instanceof BadRequest
|| $e instanceof DealApiException
|| $e instanceof ContactApiException
|| $e instanceof CompanyApiException
|| $e instanceof \GuzzleHttp\Exception\RequestException
) {
return (int) $e->getCode() === 429;
}
return false;
}
public function parseRetryAfter(Throwable $e): int
{
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$e ' . PHP_EOL . print_r($e, true));
return 5;
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;
}
}
$policy = $this->parsePolicy($e);
if ($policy === 'TEN_SECONDLY_ROLLING') {
return 10;
}
if ($policy === 'SECONDLY') {
return 1;
}
if ($policy === 'DAILY_LIMIT') {
return 600;
}
$this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [
'exception_class' => get_class($e),
]);
return 10;
}
public function parsePolicy(Throwable $e): ?string
{
if (! method_exists($e, 'getResponseBody')) {
return null;
}
$body = $e->getResponseBody();
if (is_string($body)) {
$body = json_decode($body, true) ?? [];
}
if (! is_array($body)) {
return null;
}
$policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;
return is_string($policy) ? strtoupper($policy) : null;
}
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
);
}
/**
* Execute a search request against HubSpot CRM objects with rate limiting.
*
* @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')
* @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.
* @return array The search response with 'results', 'total', 'paging' keys
* @throws RateLimitException When rate limit is hit
* @throws HubspotException On API errors
*/
public function search(string $objectType, array $payload): array
{
$endpoint = self::BASE_URL . "/crm/v3/objects/{$objectType}/search";
return $this->executeRequest(function () use ($endpoint, $payload) {
$response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
return $response->toArray();
});
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$deal = $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') {
return $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
return $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
}
/**
* @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 (RateLimitException $e) {
// throw $e;
} 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);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
17935
|
780
|
46
|
2026-05-11T10:42:13.575262+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778496133575_m1.jpg...
|
PhpStorm
|
faVsco.js – Client.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
SlackFileEditViewGoHistoryWindowHelpDOCKER₴81DEV ( SlackFileEditViewGoHistoryWindowHelpDOCKER₴81DEV (docker)Last login: Sun May 10 21:10:26 on ttys013₴2APP (-zsh)883Poetry could not find a pyproject.toml file in /Users/lukas or its parentsPoetry could not find a pyproject.toml file in /Users/lukas or its parentslukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ 0-zshHomeDMsActivityFilesLater..•More(allSupport Daily • in 1h 18 m100% <78• Mon 11 May 13:42:13ED→Describe what you are looking forJiminny ...Toast# conrusion-clinic# curiosity_lab# engineering# general# jiminny-bg# platform-tickets# product_launches# random# releases# sofia-office# support# thank-yous# the_people_of jimi...HomeMessages^ Direct messages®. Aneliya Angelova®. Galya Dimitrova& Petko Kashinski&. Stefka StoyanovaVasil Vasilev el&. Nikolay IvanovAneliya Angelova, ...Stoyan Tanev• VesE Lukas Kovalik y... 0AboutiminnyFriday, May 8th ~Added by Toast for GitHubToday ~Toast APP10:00 AMReviewapp#12059 Jy 20820 es reindexstream model hydration2 days old • 12 files changed •@Vasil VasilevAdded by Toast for GitHubResolve Conflictsapp#11443 Test hublets latency5 months old • 20 files changed#11327 JY-19501 webhookbased opportunity syncShow moreAdded by Toast for GitHubNeeds Loveapp#12024 JY-20773 fix user pilottracking ofr automated reportgenerated12 days old • 1 file changed::: AppsS Jira CloudMessage ToastToast+AaGanala CalaNew...
|
NULL
|
7026778090282363683
|
NULL
|
click
|
ocr
|
NULL
|
SlackFileEditViewGoHistoryWindowHelpDOCKER₴81DEV ( SlackFileEditViewGoHistoryWindowHelpDOCKER₴81DEV (docker)Last login: Sun May 10 21:10:26 on ttys013₴2APP (-zsh)883Poetry could not find a pyproject.toml file in /Users/lukas or its parentsPoetry could not find a pyproject.toml file in /Users/lukas or its parentslukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ 0-zshHomeDMsActivityFilesLater..•More(allSupport Daily • in 1h 18 m100% <78• Mon 11 May 13:42:13ED→Describe what you are looking forJiminny ...Toast# conrusion-clinic# curiosity_lab# engineering# general# jiminny-bg# platform-tickets# product_launches# random# releases# sofia-office# support# thank-yous# the_people_of jimi...HomeMessages^ Direct messages®. Aneliya Angelova®. Galya Dimitrova& Petko Kashinski&. Stefka StoyanovaVasil Vasilev el&. Nikolay IvanovAneliya Angelova, ...Stoyan Tanev• VesE Lukas Kovalik y... 0AboutiminnyFriday, May 8th ~Added by Toast for GitHubToday ~Toast APP10:00 AMReviewapp#12059 Jy 20820 es reindexstream model hydration2 days old • 12 files changed •@Vasil VasilevAdded by Toast for GitHubResolve Conflictsapp#11443 Test hublets latency5 months old • 20 files changed#11327 JY-19501 webhookbased opportunity syncShow moreAdded by Toast for GitHubNeeds Loveapp#12024 JY-20773 fix user pilottracking ofr automated reportgenerated12 days old • 1 file changed::: AppsS Jira CloudMessage ToastToast+AaGanala CalaNew...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
17936
|
781
|
45
|
2026-05-11T10:42:13.595430+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778496133595_m2.jpg...
|
PhpStorm
|
faVsco.js – Client.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
PhostormVIewINavicarecodeLaravelKeractorTOOISWindo PhostormVIewINavicarecodeLaravelKeractorTOOISWindowmelpFV faVsco.js°9 JY-20725-handle-HS-search-rate-limProiect© TrackRecordingFileSiz© TrackRecordingSizeEnT. ValidateSmitProspect:D AjReports0 CalendarConference0 Crm@ bullnorn0 Close0 Copper>J Crmobiects_ DecorareAcuivilyDummyHelpersv h HubspotAccountSvncStrate>D Actionsa ContactsuncStratedm Fields• Malournal1 Metadatalv OpportunitySyncSt>MConcerns.(c) Hubsnotl actMoC HubspotLastMo(C) Hubsnotl actMo© HubspotLastMo(C) Hubsnotl actMo(C) HubsnotSinaleS© HubspotSyncStr© HubspotWebhoo~ M Padination© HubspotPaginat© PaginationConfi© PaginationState> D ProspectSearchStr:› D Redisv D ServiceTraits() OpportunitvSvnc() SvncCrmEntitiesT SuncFieldstirait.T. WriteCrmTrait.o• M UtilsM WebhookC) BatchSvncCollectot(c) RatchSvncRedisSec) Client nhr(C) ClocedDea|Stadecc@ DoalFieldsService rC) TrackAutomated ReportGeneratedzventonpT SyncCrmEntitiesTrait.phpCachedcrmservicebecorator.ongoveryaeuvitycrmlaskJoo.pnp© Job.phpBadkequest.php© PaginationState.phgclass Cllent extends Baseclient 1mpLements Hubspotcllentintertace230public function getPaginatedDataGenerator(Nwroo242)243244)&. scotal,&: $lastRecordId24612472481* Execute a search request against Hubsoot cri obnects with rate limitina.2501* aparam strina SobnectiTupe The obnect tupe 'deals', 'compantes'.'contacts', 'calls')@param array<string, mixed> $payload The search payload with filters, sorts, properties, etc* Greturn arrou The search'total', "naonno'keus* Athrows RateLimitExcention When rate Limit is hitOn APt errors256 0public function search(string SobjectType, array Spayload): arraySendnnint = cpf.•RASF WIRIAneturn_Sthic->pyecuteRequest/function0uce (Sendnoint Snavloadh&Sresponse = $this->getInstance->getClient->request( method: 'POST', Sendpoint, ['json' => $payload]):return Sresponse->toArrayO:266* acnrows vealapzexceptlon* dchrows Crmexceptionpublic function getupportun1tybvld(string Scrmid, array Stields: array|/1est(fn () => $this-Sdeal = Sthis->aetNewinstance@->crmO->dealsO->basicAni@->getBvidnceo->crmo->dealso->oas1cAn1O->detividimoloded separaton• • Sfields)} catch (DealAniFycention Se) $Sthis-s1oa-sinfor1Huhsnot Satled to Fetch onnortunitvlSe->ae+Meçcane O1" suppont Dally • In 1h 10m100% LzP• Mon 11 May 13:42:13HandleHubspotRateLimitTest v« console (PROD]* console (EUl& console [STAGINGIw.19A(c) HubSpot/Service.onpA RateLimitException.ong© PaginationConfig.php743A68 V 3= laravel.log4 SF [jiminny@localhost]& HS_local [jiminny@localhost][2026-05-07 14:21:15] local.INF0: [Hubspot] DEBUG Getting headers {"Date":["Thu,07 May 2026 14:21:15 GMT"],"Content-Type":["application/json;charset=utf-8"]."Transfer-Encoding": ["chunked"],"conneccion". Keep-alive"n"CF-Ray" : ["9f80deb8db60dc3a-SOF"],"CF-Cache-Status":"DYNAMIC")"Strict-Transport-Secur1ty":"max-aqe=31536000* 1ncludeSubDomains: preload")accept-encoding"],"access-control-allow-credentials": "false").server-timing": ["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",cfn:desc="9-80de8ercodc3a-TAD""x-content-type-options": ["nosniff"]."x-hubsoot-correlation-id":"019e02d0-6fd8-7812-bdba-885b7ccb3ee3"])"Set-Cookie".[" cf bm=STUrtd0aXVrik50odaF6hZVYKhzTn0BidvMabeCtm0Y-1778163675-[IP_ADDRESS]-рT. ZaatDKxTae5zr8 2abBfWM00. ufZEXDZuHz2mBUFdzdo2aTHEs0)07-May-26 14:51:15 GMT; domain=.hubapi.com; Http0nly; Secure; SameSite=None"],"Report-To":["1"endnoints"•!\"url\":\"https:|V/\V/a.nel.cloudflare.com/\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZLzoYdxI%2BIxVpHmsKn30%2BKVA3mFIJ2m7YRECDGS\"group\":\"cf-nel\",\"max_age\":604800}"],"success_fraction\":0.01,"report_to\":\"cf-nel\"\"max_age\":604800}"],"Server": ["cloudflare"]}} {"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab"."trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}I MIm T T111W Windsurf Teams 256:21 UTF-8 P 4 spaces ®...
|
NULL
|
-4974032155204697394
|
NULL
|
click
|
ocr
|
NULL
|
PhostormVIewINavicarecodeLaravelKeractorTOOISWindo PhostormVIewINavicarecodeLaravelKeractorTOOISWindowmelpFV faVsco.js°9 JY-20725-handle-HS-search-rate-limProiect© TrackRecordingFileSiz© TrackRecordingSizeEnT. ValidateSmitProspect:D AjReports0 CalendarConference0 Crm@ bullnorn0 Close0 Copper>J Crmobiects_ DecorareAcuivilyDummyHelpersv h HubspotAccountSvncStrate>D Actionsa ContactsuncStratedm Fields• Malournal1 Metadatalv OpportunitySyncSt>MConcerns.(c) Hubsnotl actMoC HubspotLastMo(C) Hubsnotl actMo© HubspotLastMo(C) Hubsnotl actMo(C) HubsnotSinaleS© HubspotSyncStr© HubspotWebhoo~ M Padination© HubspotPaginat© PaginationConfi© PaginationState> D ProspectSearchStr:› D Redisv D ServiceTraits() OpportunitvSvnc() SvncCrmEntitiesT SuncFieldstirait.T. WriteCrmTrait.o• M UtilsM WebhookC) BatchSvncCollectot(c) RatchSvncRedisSec) Client nhr(C) ClocedDea|Stadecc@ DoalFieldsService rC) TrackAutomated ReportGeneratedzventonpT SyncCrmEntitiesTrait.phpCachedcrmservicebecorator.ongoveryaeuvitycrmlaskJoo.pnp© Job.phpBadkequest.php© PaginationState.phgclass Cllent extends Baseclient 1mpLements Hubspotcllentintertace230public function getPaginatedDataGenerator(Nwroo242)243244)&. scotal,&: $lastRecordId24612472481* Execute a search request against Hubsoot cri obnects with rate limitina.2501* aparam strina SobnectiTupe The obnect tupe 'deals', 'compantes'.'contacts', 'calls')@param array<string, mixed> $payload The search payload with filters, sorts, properties, etc* Greturn arrou The search'total', "naonno'keus* Athrows RateLimitExcention When rate Limit is hitOn APt errors256 0public function search(string SobjectType, array Spayload): arraySendnnint = cpf.•RASF WIRIAneturn_Sthic->pyecuteRequest/function0uce (Sendnoint Snavloadh&Sresponse = $this->getInstance->getClient->request( method: 'POST', Sendpoint, ['json' => $payload]):return Sresponse->toArrayO:266* acnrows vealapzexceptlon* dchrows Crmexceptionpublic function getupportun1tybvld(string Scrmid, array Stields: array|/1est(fn () => $this-Sdeal = Sthis->aetNewinstance@->crmO->dealsO->basicAni@->getBvidnceo->crmo->dealso->oas1cAn1O->detividimoloded separaton• • Sfields)} catch (DealAniFycention Se) $Sthis-s1oa-sinfor1Huhsnot Satled to Fetch onnortunitvlSe->ae+Meçcane O1" suppont Dally • In 1h 10m100% LzP• Mon 11 May 13:42:13HandleHubspotRateLimitTest v« console (PROD]* console (EUl& console [STAGINGIw.19A(c) HubSpot/Service.onpA RateLimitException.ong© PaginationConfig.php743A68 V 3= laravel.log4 SF [jiminny@localhost]& HS_local [jiminny@localhost][2026-05-07 14:21:15] local.INF0: [Hubspot] DEBUG Getting headers {"Date":["Thu,07 May 2026 14:21:15 GMT"],"Content-Type":["application/json;charset=utf-8"]."Transfer-Encoding": ["chunked"],"conneccion". Keep-alive"n"CF-Ray" : ["9f80deb8db60dc3a-SOF"],"CF-Cache-Status":"DYNAMIC")"Strict-Transport-Secur1ty":"max-aqe=31536000* 1ncludeSubDomains: preload")accept-encoding"],"access-control-allow-credentials": "false").server-timing": ["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",cfn:desc="9-80de8ercodc3a-TAD""x-content-type-options": ["nosniff"]."x-hubsoot-correlation-id":"019e02d0-6fd8-7812-bdba-885b7ccb3ee3"])"Set-Cookie".[" cf bm=STUrtd0aXVrik50odaF6hZVYKhzTn0BidvMabeCtm0Y-1778163675-[IP_ADDRESS]-рT. ZaatDKxTae5zr8 2abBfWM00. ufZEXDZuHz2mBUFdzdo2aTHEs0)07-May-26 14:51:15 GMT; domain=.hubapi.com; Http0nly; Secure; SameSite=None"],"Report-To":["1"endnoints"•!\"url\":\"https:|V/\V/a.nel.cloudflare.com/\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZLzoYdxI%2BIxVpHmsKn30%2BKVA3mFIJ2m7YRECDGS\"group\":\"cf-nel\",\"max_age\":604800}"],"success_fraction\":0.01,"report_to\":\"cf-nel\"\"max_age\":604800}"],"Server": ["cloudflare"]}} {"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab"."trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}I MIm T T111W Windsurf Teams 256:21 UTF-8 P 4 spaces ®...
|
17934
|
NULL
|
NULL
|
NULL
|
|
17937
|
780
|
47
|
2026-05-11T10:42:18.325329+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778496138325_m1.jpg...
|
PhpStorm
|
faVsco.js – Client.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
3
68
3
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\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 Illuminate\Support\Facades\Redis;
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)
{
$cacheKey = $this->getRateLimitCacheKey();
$cachedRetryAfter = Redis::get($cacheKey);
if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {
throw new RateLimitException(
'Hubspot rate limit (cached circuit-breaker)',
(int) $cachedRetryAfter,
);
}
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'policy' => $this->parsePolicy($e),
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function getRateLimitCacheKey(): string
{
return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());
}
public function isHubspotRateLimit(Throwable $e): bool
{
if ($e instanceof BadRequest
|| $e instanceof DealApiException
|| $e instanceof ContactApiException
|| $e instanceof CompanyApiException
|| $e instanceof \GuzzleHttp\Exception\RequestException
) {
return (int) $e->getCode() === 429;
}
return false;
}
public function parseRetryAfter(Throwable $e): int
{
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$e ' . PHP_EOL . print_r($e, true));
return 5;
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;
}
}
$policy = $this->parsePolicy($e);
if ($policy === 'TEN_SECONDLY_ROLLING') {
return 10;
}
if ($policy === 'SECONDLY') {
return 1;
}
if ($policy === 'DAILY_LIMIT') {
return 600;
}
$this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [
'exception_class' => get_class($e),
]);
return 10;
}
public function parsePolicy(Throwable $e): ?string
{
if (! method_exists($e, 'getResponseBody')) {
return null;
}
$body = $e->getResponseBody();
if (is_string($body)) {
$body = json_decode($body, true) ?? [];
}
if (! is_array($body)) {
return null;
}
$policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;
return is_string($policy) ? strtoupper($policy) : null;
}
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
);
}
/**
* Execute a search request against HubSpot CRM objects with rate limiting.
*
* @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')
* @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.
* @return array The search response with 'results', 'total', 'paging' keys
* @throws RateLimitException When rate limit is hit
* @throws HubspotException On API errors
*/
public function search(string $objectType, array $payload): array
{
$endpoint = self::BASE_URL . "/crm/v3/objects/{$objectType}/search";
return $this->executeRequest(function () use ($endpoint, $payload) {
$response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
return $response->toArray();
});
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$deal = $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') {
return $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
return $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
}
/**
* @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 (RateLimitException $e) {
// throw $e;
} 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);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
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":"JY-20725-handle-HS-search-rate-limit, menu","depth":5,"on_screen":true,"help_text":"Git Branch: JY-20725-handle-HS-search-rate-limit","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":"HandleHubspotRateLimitTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'HandleHubspotRateLimitTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'HandleHubspotRateLimitTest'","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":"AXStaticText","text":"3","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"68","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"3","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"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\\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 Illuminate\\Support\\Facades\\Redis;\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\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Reacts to a rate limits (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 $cacheKey = $this->getRateLimitCacheKey();\n\n $cachedRetryAfter = Redis::get($cacheKey);\n if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n (int) $cachedRetryAfter,\n );\n }\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);\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 'policy' => $this->parsePolicy($e),\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 getRateLimitCacheKey(): string\n {\n return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());\n }\n\n public function isHubspotRateLimit(Throwable $e): bool\n {\n if ($e instanceof BadRequest\n || $e instanceof DealApiException\n || $e instanceof ContactApiException\n || $e instanceof CompanyApiException\n || $e instanceof \\GuzzleHttp\\Exception\\RequestException\n ) {\n return (int) $e->getCode() === 429;\n }\n\n return false;\n }\n\n public function parseRetryAfter(Throwable $e): int\n {\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$e ' . PHP_EOL . print_r($e, true));\n return 5;\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 $policy = $this->parsePolicy($e);\n if ($policy === 'TEN_SECONDLY_ROLLING') {\n return 10;\n }\n if ($policy === 'SECONDLY') {\n return 1;\n }\n if ($policy === 'DAILY_LIMIT') {\n return 600;\n }\n\n $this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [\n 'exception_class' => get_class($e),\n ]);\n\n return 10;\n }\n\n public function parsePolicy(Throwable $e): ?string\n {\n if (! method_exists($e, 'getResponseBody')) {\n return null;\n }\n\n $body = $e->getResponseBody();\n if (is_string($body)) {\n $body = json_decode($body, true) ?? [];\n }\n\n if (! is_array($body)) {\n return null;\n }\n\n $policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;\n\n return is_string($policy) ? strtoupper($policy) : null;\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 * Execute a search request against HubSpot CRM objects with rate limiting.\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n * @return array The search response with 'results', 'total', 'paging' keys\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n */\n public function search(string $objectType, array $payload): array\n {\n $endpoint = self::BASE_URL . \"/crm/v3/objects/{$objectType}/search\";\n\n return $this->executeRequest(function () use ($endpoint, $payload) {\n $response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n return $response->toArray();\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 $deal = $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 return $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n return $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\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 (RateLimitException $e) {\n// throw $e;\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\\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 Illuminate\\Support\\Facades\\Redis;\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\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Reacts to a rate limits (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 $cacheKey = $this->getRateLimitCacheKey();\n\n $cachedRetryAfter = Redis::get($cacheKey);\n if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n (int) $cachedRetryAfter,\n );\n }\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);\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 'policy' => $this->parsePolicy($e),\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 getRateLimitCacheKey(): string\n {\n return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());\n }\n\n public function isHubspotRateLimit(Throwable $e): bool\n {\n if ($e instanceof BadRequest\n || $e instanceof DealApiException\n || $e instanceof ContactApiException\n || $e instanceof CompanyApiException\n || $e instanceof \\GuzzleHttp\\Exception\\RequestException\n ) {\n return (int) $e->getCode() === 429;\n }\n\n return false;\n }\n\n public function parseRetryAfter(Throwable $e): int\n {\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$e ' . PHP_EOL . print_r($e, true));\n return 5;\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 $policy = $this->parsePolicy($e);\n if ($policy === 'TEN_SECONDLY_ROLLING') {\n return 10;\n }\n if ($policy === 'SECONDLY') {\n return 1;\n }\n if ($policy === 'DAILY_LIMIT') {\n return 600;\n }\n\n $this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [\n 'exception_class' => get_class($e),\n ]);\n\n return 10;\n }\n\n public function parsePolicy(Throwable $e): ?string\n {\n if (! method_exists($e, 'getResponseBody')) {\n return null;\n }\n\n $body = $e->getResponseBody();\n if (is_string($body)) {\n $body = json_decode($body, true) ?? [];\n }\n\n if (! is_array($body)) {\n return null;\n }\n\n $policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;\n\n return is_string($policy) ? strtoupper($policy) : null;\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 * Execute a search request against HubSpot CRM objects with rate limiting.\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n * @return array The search response with 'results', 'total', 'paging' keys\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n */\n public function search(string $objectType, array $payload): array\n {\n $endpoint = self::BASE_URL . \"/crm/v3/objects/{$objectType}/search\";\n\n return $this->executeRequest(function () use ($endpoint, $payload) {\n $response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n return $response->toArray();\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 $deal = $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 return $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n return $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\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 (RateLimitException $e) {\n// throw $e;\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":"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":"19","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","depth":4,"on_screen":true,"value":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","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}]...
|
-6243931637791501581
|
6378618611371477092
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
3
68
3
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\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 Illuminate\Support\Facades\Redis;
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)
{
$cacheKey = $this->getRateLimitCacheKey();
$cachedRetryAfter = Redis::get($cacheKey);
if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {
throw new RateLimitException(
'Hubspot rate limit (cached circuit-breaker)',
(int) $cachedRetryAfter,
);
}
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'policy' => $this->parsePolicy($e),
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function getRateLimitCacheKey(): string
{
return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());
}
public function isHubspotRateLimit(Throwable $e): bool
{
if ($e instanceof BadRequest
|| $e instanceof DealApiException
|| $e instanceof ContactApiException
|| $e instanceof CompanyApiException
|| $e instanceof \GuzzleHttp\Exception\RequestException
) {
return (int) $e->getCode() === 429;
}
return false;
}
public function parseRetryAfter(Throwable $e): int
{
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$e ' . PHP_EOL . print_r($e, true));
return 5;
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;
}
}
$policy = $this->parsePolicy($e);
if ($policy === 'TEN_SECONDLY_ROLLING') {
return 10;
}
if ($policy === 'SECONDLY') {
return 1;
}
if ($policy === 'DAILY_LIMIT') {
return 600;
}
$this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [
'exception_class' => get_class($e),
]);
return 10;
}
public function parsePolicy(Throwable $e): ?string
{
if (! method_exists($e, 'getResponseBody')) {
return null;
}
$body = $e->getResponseBody();
if (is_string($body)) {
$body = json_decode($body, true) ?? [];
}
if (! is_array($body)) {
return null;
}
$policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;
return is_string($policy) ? strtoupper($policy) : null;
}
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
);
}
/**
* Execute a search request against HubSpot CRM objects with rate limiting.
*
* @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')
* @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.
* @return array The search response with 'results', 'total', 'paging' keys
* @throws RateLimitException When rate limit is hit
* @throws HubspotException On API errors
*/
public function search(string $objectType, array $payload): array
{
$endpoint = self::BASE_URL . "/crm/v3/objects/{$objectType}/search";
return $this->executeRequest(function () use ($endpoint, $payload) {
$response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
return $response->toArray();
});
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$deal = $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') {
return $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
return $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
}
/**
* @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 (RateLimitException $e) {
// throw $e;
} 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);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
17935
|
NULL
|
NULL
|
NULL
|
|
17938
|
781
|
46
|
2026-05-11T10:42:18.325369+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778496138325_m2.jpg...
|
PhpStorm
|
faVsco.js – Client.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
3
68
3
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\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 Illuminate\Support\Facades\Redis;
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)
{
$cacheKey = $this->getRateLimitCacheKey();
$cachedRetryAfter = Redis::get($cacheKey);
if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {
throw new RateLimitException(
'Hubspot rate limit (cached circuit-breaker)',
(int) $cachedRetryAfter,
);
}
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'policy' => $this->parsePolicy($e),
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function getRateLimitCacheKey(): string
{
return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());
}
public function isHubspotRateLimit(Throwable $e): bool
{
if ($e instanceof BadRequest
|| $e instanceof DealApiException
|| $e instanceof ContactApiException
|| $e instanceof CompanyApiException
|| $e instanceof \GuzzleHttp\Exception\RequestException
) {
return (int) $e->getCode() === 429;
}
return false;
}
public function parseRetryAfter(Throwable $e): int
{
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$e ' . PHP_EOL . print_r($e, true));
return 5;
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;
}
}
$policy = $this->parsePolicy($e);
if ($policy === 'TEN_SECONDLY_ROLLING') {
return 10;
}
if ($policy === 'SECONDLY') {
return 1;
}
if ($policy === 'DAILY_LIMIT') {
return 600;
}
$this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [
'exception_class' => get_class($e),
]);
return 10;
}
public function parsePolicy(Throwable $e): ?string
{
if (! method_exists($e, 'getResponseBody')) {
return null;
}
$body = $e->getResponseBody();
if (is_string($body)) {
$body = json_decode($body, true) ?? [];
}
if (! is_array($body)) {
return null;
}
$policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;
return is_string($policy) ? strtoupper($policy) : null;
}
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
);
}
/**
* Execute a search request against HubSpot CRM objects with rate limiting.
*
* @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')
* @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.
* @return array The search response with 'results', 'total', 'paging' keys
* @throws RateLimitException When rate limit is hit
* @throws HubspotException On API errors
*/
public function search(string $objectType, array $payload): array
{
$endpoint = self::BASE_URL . "/crm/v3/objects/{$objectType}/search";
return $this->executeRequest(function () use ($endpoint, $payload) {
$response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
return $response->toArray();
});
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$deal = $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') {
return $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
return $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
}
/**
* @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 (RateLimitException $e) {
// throw $e;
} 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);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
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":"JY-20725-handle-HS-search-rate-limit, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.09541223,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: JY-20725-handle-HS-search-rate-limit","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.82413566,"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":"HandleHubspotRateLimitTest","depth":6,"bounds":{"left":0.8394282,"top":0.019952115,"width":0.076130316,"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 'HandleHubspotRateLimitTest'","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 'HandleHubspotRateLimitTest'","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":"AXStaticText","text":"3","depth":4,"bounds":{"left":0.5515292,"top":0.15003991,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"68","depth":4,"bounds":{"left":0.56150264,"top":0.15003991,"width":0.010305851,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"3","depth":4,"bounds":{"left":0.5738032,"top":0.15003991,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.5834442,"top":0.14844373,"width":0.00731383,"height":0.018355945},"on_screen":true,"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.59075797,"top":0.14844373,"width":0.006981383,"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\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\\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 Illuminate\\Support\\Facades\\Redis;\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\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Reacts to a rate limits (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 $cacheKey = $this->getRateLimitCacheKey();\n\n $cachedRetryAfter = Redis::get($cacheKey);\n if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n (int) $cachedRetryAfter,\n );\n }\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);\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 'policy' => $this->parsePolicy($e),\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 getRateLimitCacheKey(): string\n {\n return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());\n }\n\n public function isHubspotRateLimit(Throwable $e): bool\n {\n if ($e instanceof BadRequest\n || $e instanceof DealApiException\n || $e instanceof ContactApiException\n || $e instanceof CompanyApiException\n || $e instanceof \\GuzzleHttp\\Exception\\RequestException\n ) {\n return (int) $e->getCode() === 429;\n }\n\n return false;\n }\n\n public function parseRetryAfter(Throwable $e): int\n {\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$e ' . PHP_EOL . print_r($e, true));\n return 5;\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 $policy = $this->parsePolicy($e);\n if ($policy === 'TEN_SECONDLY_ROLLING') {\n return 10;\n }\n if ($policy === 'SECONDLY') {\n return 1;\n }\n if ($policy === 'DAILY_LIMIT') {\n return 600;\n }\n\n $this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [\n 'exception_class' => get_class($e),\n ]);\n\n return 10;\n }\n\n public function parsePolicy(Throwable $e): ?string\n {\n if (! method_exists($e, 'getResponseBody')) {\n return null;\n }\n\n $body = $e->getResponseBody();\n if (is_string($body)) {\n $body = json_decode($body, true) ?? [];\n }\n\n if (! is_array($body)) {\n return null;\n }\n\n $policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;\n\n return is_string($policy) ? strtoupper($policy) : null;\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 * Execute a search request against HubSpot CRM objects with rate limiting.\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n * @return array The search response with 'results', 'total', 'paging' keys\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n */\n public function search(string $objectType, array $payload): array\n {\n $endpoint = self::BASE_URL . \"/crm/v3/objects/{$objectType}/search\";\n\n return $this->executeRequest(function () use ($endpoint, $payload) {\n $response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n return $response->toArray();\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 $deal = $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 return $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n return $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\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 (RateLimitException $e) {\n// throw $e;\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\\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 Illuminate\\Support\\Facades\\Redis;\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\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Reacts to a rate limits (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 $cacheKey = $this->getRateLimitCacheKey();\n\n $cachedRetryAfter = Redis::get($cacheKey);\n if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n (int) $cachedRetryAfter,\n );\n }\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);\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 'policy' => $this->parsePolicy($e),\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 getRateLimitCacheKey(): string\n {\n return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());\n }\n\n public function isHubspotRateLimit(Throwable $e): bool\n {\n if ($e instanceof BadRequest\n || $e instanceof DealApiException\n || $e instanceof ContactApiException\n || $e instanceof CompanyApiException\n || $e instanceof \\GuzzleHttp\\Exception\\RequestException\n ) {\n return (int) $e->getCode() === 429;\n }\n\n return false;\n }\n\n public function parseRetryAfter(Throwable $e): int\n {\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$e ' . PHP_EOL . print_r($e, true));\n return 5;\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 $policy = $this->parsePolicy($e);\n if ($policy === 'TEN_SECONDLY_ROLLING') {\n return 10;\n }\n if ($policy === 'SECONDLY') {\n return 1;\n }\n if ($policy === 'DAILY_LIMIT') {\n return 600;\n }\n\n $this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [\n 'exception_class' => get_class($e),\n ]);\n\n return 10;\n }\n\n public function parsePolicy(Throwable $e): ?string\n {\n if (! method_exists($e, 'getResponseBody')) {\n return null;\n }\n\n $body = $e->getResponseBody();\n if (is_string($body)) {\n $body = json_decode($body, true) ?? [];\n }\n\n if (! is_array($body)) {\n return null;\n }\n\n $policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;\n\n return is_string($policy) ? strtoupper($policy) : null;\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 * Execute a search request against HubSpot CRM objects with rate limiting.\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n * @return array The search response with 'results', 'total', 'paging' keys\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n */\n public function search(string $objectType, array $payload): array\n {\n $endpoint = self::BASE_URL . \"/crm/v3/objects/{$objectType}/search\";\n\n return $this->executeRequest(function () use ($endpoint, $payload) {\n $response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n return $response->toArray();\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 $deal = $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 return $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n return $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\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 (RateLimitException $e) {\n// throw $e;\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":"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":"19","depth":4,"bounds":{"left":0.96276593,"top":0.07581804,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.9740692,"top":0.074221864,"width":0.00731383,"height":0.018355945},"on_screen":true,"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.98138297,"top":0.074221864,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","depth":4,"bounds":{"left":0.61702126,"top":0.0726257,"width":0.38297874,"height":0.9066241},"on_screen":true,"lines":[{"char_start":273,"char_count":32,"bounds":{"left":0.61702126,"top":0.0,"width":0.080119684,"height":0.014365523}},{"char_start":305,"char_count":79,"bounds":{"left":0.61702126,"top":0.0,"width":0.20212767,"height":0.014365523}},{"char_start":384,"char_count":18,"bounds":{"left":0.61702126,"top":0.0,"width":0.043882977,"height":0.014365523}},{"char_start":402,"char_count":21,"bounds":{"left":0.61702126,"top":0.0,"width":0.051861703,"height":0.014365523}},{"char_start":423,"char_count":48,"bounds":{"left":0.61702126,"top":0.0,"width":0.12167553,"height":0.014365523}},{"char_start":471,"char_count":72,"bounds":{"left":0.61702126,"top":0.0015961692,"width":0.18384309,"height":0.014365523}},{"char_start":543,"char_count":40,"bounds":{"left":0.61702126,"top":0.01915403,"width":0.10106383,"height":0.014365523}},{"char_start":583,"char_count":41,"bounds":{"left":0.61702126,"top":0.03671189,"width":0.10372341,"height":0.014365523}},{"char_start":624,"char_count":72,"bounds":{"left":0.61702126,"top":0.054269753,"width":0.18384309,"height":0.014365523}},{"char_start":696,"char_count":219,"bounds":{"left":0.61702126,"top":0.07182761,"width":0.38297874,"height":0.014365523}},{"char_start":915,"char_count":83,"bounds":{"left":0.61702126,"top":0.08938547,"width":0.21243352,"height":0.014365523}},{"char_start":998,"char_count":20,"bounds":{"left":0.61702126,"top":0.10694334,"width":0.04920213,"height":0.014365523}},{"char_start":1018,"char_count":17,"bounds":{"left":0.61702126,"top":0.1245012,"width":0.041223403,"height":0.014365523}},{"char_start":1035,"char_count":203,"bounds":{"left":0.61702126,"top":0.14205906,"width":0.38297874,"height":0.014365523}},{"char_start":1238,"char_count":22,"bounds":{"left":0.61702126,"top":0.15961692,"width":0.05418883,"height":0.014365523}},{"char_start":1260,"char_count":23,"bounds":{"left":0.61702126,"top":0.17717478,"width":0.056848403,"height":0.014365523}},{"char_start":1283,"char_count":10,"bounds":{"left":0.61702126,"top":0.19473264,"width":0.023271276,"height":0.014365523}},{"char_start":1293,"char_count":27,"bounds":{"left":0.61702126,"top":0.2122905,"width":0.06715426,"height":0.014365523}},{"char_start":1320,"char_count":26,"bounds":{"left":0.61702126,"top":0.22984837,"width":0.06482713,"height":0.014365523}},{"char_start":1346,"char_count":23,"bounds":{"left":0.61702126,"top":0.24740623,"width":0.056848403,"height":0.014365523}},{"char_start":1369,"char_count":28,"bounds":{"left":0.61702126,"top":0.26496407,"width":0.06981383,"height":0.014365523}},{"char_start":1397,"char_count":57,"bounds":{"left":0.61702126,"top":0.28252193,"width":0.14494681,"height":0.014365523}}],"value":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","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}]...
|
-6243931637791501581
|
6378618611371477092
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
3
68
3
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\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 Illuminate\Support\Facades\Redis;
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)
{
$cacheKey = $this->getRateLimitCacheKey();
$cachedRetryAfter = Redis::get($cacheKey);
if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {
throw new RateLimitException(
'Hubspot rate limit (cached circuit-breaker)',
(int) $cachedRetryAfter,
);
}
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'policy' => $this->parsePolicy($e),
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function getRateLimitCacheKey(): string
{
return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());
}
public function isHubspotRateLimit(Throwable $e): bool
{
if ($e instanceof BadRequest
|| $e instanceof DealApiException
|| $e instanceof ContactApiException
|| $e instanceof CompanyApiException
|| $e instanceof \GuzzleHttp\Exception\RequestException
) {
return (int) $e->getCode() === 429;
}
return false;
}
public function parseRetryAfter(Throwable $e): int
{
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$e ' . PHP_EOL . print_r($e, true));
return 5;
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;
}
}
$policy = $this->parsePolicy($e);
if ($policy === 'TEN_SECONDLY_ROLLING') {
return 10;
}
if ($policy === 'SECONDLY') {
return 1;
}
if ($policy === 'DAILY_LIMIT') {
return 600;
}
$this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [
'exception_class' => get_class($e),
]);
return 10;
}
public function parsePolicy(Throwable $e): ?string
{
if (! method_exists($e, 'getResponseBody')) {
return null;
}
$body = $e->getResponseBody();
if (is_string($body)) {
$body = json_decode($body, true) ?? [];
}
if (! is_array($body)) {
return null;
}
$policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;
return is_string($policy) ? strtoupper($policy) : null;
}
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
);
}
/**
* Execute a search request against HubSpot CRM objects with rate limiting.
*
* @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')
* @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.
* @return array The search response with 'results', 'total', 'paging' keys
* @throws RateLimitException When rate limit is hit
* @throws HubspotException On API errors
*/
public function search(string $objectType, array $payload): array
{
$endpoint = self::BASE_URL . "/crm/v3/objects/{$objectType}/search";
return $this->executeRequest(function () use ($endpoint, $payload) {
$response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
return $response->toArray();
});
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$deal = $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') {
return $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
return $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
}
/**
* @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 (RateLimitException $e) {
// throw $e;
} 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);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
17939
|
780
|
48
|
2026-05-11T10:42:22.929193+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778496142929_m1.jpg...
|
PhpStorm
|
faVsco.js – Client.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
|
[{"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}]...
|
8043719072324535154
|
-8628527368849355612
|
click
|
hybrid
|
NULL
|
Project: faVsco.js, menu
SlackFileEditViewGoHistor Project: faVsco.js, menu
SlackFileEditViewGoHistoryWindowHelpDOCKER→-₴1DEV (docker)Last login: Sun May 10 21:10:26 on ttys013₴2APP (-zsh)883Poetry could not find a pyproject.toml file in /Users/lukas or its parentsPoetry could not find a pyproject.toml file in /Users/lukas or its parentslukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ 0-zshHomeDMsActivityFilesLater..•More(allSupport Daily • in 1h 18 m100% <78• Mon 11 May 13:42:22ED→Describe what you are looking forJiminny ...Toast# conrusion-clinic# curiosity_lab# engineering# general# jiminny-bg# platform-tickets# product_launches# random# releases# sofia-office# support# thank-yous# the_people_of jimi...HomeMessages^ Direct messages®. Aneliya Angelova®. Galya Dimitrova& Petko Kashinski&. Stefka StoyanovaVasil Vasilev el&. Nikolay IvanovAneliya Angelova, ...Stoyan Tanev. VesE Lukas Kovalik y... 0AboutiminnyFriday, May 8th ~Added by Toast for GitHubToday ~Toast APP10:00 AMReviewapp#12059 Jy 20820 es reindexstream model hydration2 days old • 12 files changed •@Vasil VasilevAdded by Toast for GitHubResolve Conflictsapp#11443 Test hublets latency5 months old • 20 files changed#11327 JY-19501 webhookbased opportunity syncShow moreAdded by Toast for GitHubNeeds Loveapp#12024 JY-20773 fix user pilottracking ofr automated reportgenerated12 days old • 1 file changed::: AppsS Jira CloudMessage ToastToast+AaGanala CalaNew...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
17940
|
780
|
49
|
2026-05-11T10:42:24.813331+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778496144813_m1.jpg...
|
PhpStorm
|
faVsco.js – Client.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
3
68
3
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\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 Illuminate\Support\Facades\Redis;
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)
{
$cacheKey = $this->getRateLimitCacheKey();
$cachedRetryAfter = Redis::get($cacheKey);
if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {
throw new RateLimitException(
'Hubspot rate limit (cached circuit-breaker)',
(int) $cachedRetryAfter,
);
}
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'policy' => $this->parsePolicy($e),
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function getRateLimitCacheKey(): string
{
return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());
}
public function isHubspotRateLimit(Throwable $e): bool
{
if ($e instanceof BadRequest
|| $e instanceof DealApiException
|| $e instanceof ContactApiException
|| $e instanceof CompanyApiException
|| $e instanceof \GuzzleHttp\Exception\RequestException
) {
return (int) $e->getCode() === 429;
}
return false;
}
public function parseRetryAfter(Throwable $e): int
{
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$e ' . PHP_EOL . print_r($e, true));
return 5;
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;
}
}
$policy = $this->parsePolicy($e);
if ($policy === 'TEN_SECONDLY_ROLLING') {
return 10;
}
if ($policy === 'SECONDLY') {
return 1;
}
if ($policy === 'DAILY_LIMIT') {
return 600;
}
$this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [
'exception_class' => get_class($e),
]);
return 10;
}
public function parsePolicy(Throwable $e): ?string
{
if (! method_exists($e, 'getResponseBody')) {
return null;
}
$body = $e->getResponseBody();
if (is_string($body)) {
$body = json_decode($body, true) ?? [];
}
if (! is_array($body)) {
return null;
}
$policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;
return is_string($policy) ? strtoupper($policy) : null;
}
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
);
}
/**
* Execute a search request against HubSpot CRM objects with rate limiting.
*
* @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')
* @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.
* @return array The search response with 'results', 'total', 'paging' keys
* @throws RateLimitException When rate limit is hit
* @throws HubspotException On API errors
*/
public function search(string $objectType, array $payload): array
{
$endpoint = self::BASE_URL . "/crm/v3/objects/{$objectType}/search";
return $this->executeRequest(function () use ($endpoint, $payload) {
$response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$response ' . PHP_EOL . print_r($response, true));
return $response->toArray();
});
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$deal = $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') {
return $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
return $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
}
/**
* @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 (RateLimitException $e) {
// throw $e;
} 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);
}
}...
|
[{"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":"JY-20725-handle-HS-search-rate-limit, menu","depth":5,"on_screen":true,"help_text":"Git Branch: JY-20725-handle-HS-search-rate-limit","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":"HandleHubspotRateLimitTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'HandleHubspotRateLimitTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'HandleHubspotRateLimitTest'","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":"AXStaticText","text":"3","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"68","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"3","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"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\\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 Illuminate\\Support\\Facades\\Redis;\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\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Reacts to a rate limits (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 $cacheKey = $this->getRateLimitCacheKey();\n\n $cachedRetryAfter = Redis::get($cacheKey);\n if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n (int) $cachedRetryAfter,\n );\n }\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);\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 'policy' => $this->parsePolicy($e),\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 getRateLimitCacheKey(): string\n {\n return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());\n }\n\n public function isHubspotRateLimit(Throwable $e): bool\n {\n if ($e instanceof BadRequest\n || $e instanceof DealApiException\n || $e instanceof ContactApiException\n || $e instanceof CompanyApiException\n || $e instanceof \\GuzzleHttp\\Exception\\RequestException\n ) {\n return (int) $e->getCode() === 429;\n }\n\n return false;\n }\n\n public function parseRetryAfter(Throwable $e): int\n {\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$e ' . PHP_EOL . print_r($e, true));\n return 5;\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 $policy = $this->parsePolicy($e);\n if ($policy === 'TEN_SECONDLY_ROLLING') {\n return 10;\n }\n if ($policy === 'SECONDLY') {\n return 1;\n }\n if ($policy === 'DAILY_LIMIT') {\n return 600;\n }\n\n $this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [\n 'exception_class' => get_class($e),\n ]);\n\n return 10;\n }\n\n public function parsePolicy(Throwable $e): ?string\n {\n if (! method_exists($e, 'getResponseBody')) {\n return null;\n }\n\n $body = $e->getResponseBody();\n if (is_string($body)) {\n $body = json_decode($body, true) ?? [];\n }\n\n if (! is_array($body)) {\n return null;\n }\n\n $policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;\n\n return is_string($policy) ? strtoupper($policy) : null;\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 * Execute a search request against HubSpot CRM objects with rate limiting.\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n * @return array The search response with 'results', 'total', 'paging' keys\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n */\n public function search(string $objectType, array $payload): array\n {\n $endpoint = self::BASE_URL . \"/crm/v3/objects/{$objectType}/search\";\n\n return $this->executeRequest(function () use ($endpoint, $payload) {\n $response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$response ' . PHP_EOL . print_r($response, true));\n\n return $response->toArray();\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 $deal = $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 return $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n return $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\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 (RateLimitException $e) {\n// throw $e;\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\\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 Illuminate\\Support\\Facades\\Redis;\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\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Reacts to a rate limits (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 $cacheKey = $this->getRateLimitCacheKey();\n\n $cachedRetryAfter = Redis::get($cacheKey);\n if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n (int) $cachedRetryAfter,\n );\n }\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);\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 'policy' => $this->parsePolicy($e),\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 getRateLimitCacheKey(): string\n {\n return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());\n }\n\n public function isHubspotRateLimit(Throwable $e): bool\n {\n if ($e instanceof BadRequest\n || $e instanceof DealApiException\n || $e instanceof ContactApiException\n || $e instanceof CompanyApiException\n || $e instanceof \\GuzzleHttp\\Exception\\RequestException\n ) {\n return (int) $e->getCode() === 429;\n }\n\n return false;\n }\n\n public function parseRetryAfter(Throwable $e): int\n {\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$e ' . PHP_EOL . print_r($e, true));\n return 5;\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 $policy = $this->parsePolicy($e);\n if ($policy === 'TEN_SECONDLY_ROLLING') {\n return 10;\n }\n if ($policy === 'SECONDLY') {\n return 1;\n }\n if ($policy === 'DAILY_LIMIT') {\n return 600;\n }\n\n $this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [\n 'exception_class' => get_class($e),\n ]);\n\n return 10;\n }\n\n public function parsePolicy(Throwable $e): ?string\n {\n if (! method_exists($e, 'getResponseBody')) {\n return null;\n }\n\n $body = $e->getResponseBody();\n if (is_string($body)) {\n $body = json_decode($body, true) ?? [];\n }\n\n if (! is_array($body)) {\n return null;\n }\n\n $policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;\n\n return is_string($policy) ? strtoupper($policy) : null;\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 * Execute a search request against HubSpot CRM objects with rate limiting.\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n * @return array The search response with 'results', 'total', 'paging' keys\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n */\n public function search(string $objectType, array $payload): array\n {\n $endpoint = self::BASE_URL . \"/crm/v3/objects/{$objectType}/search\";\n\n return $this->executeRequest(function () use ($endpoint, $payload) {\n $response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$response ' . PHP_EOL . print_r($response, true));\n\n return $response->toArray();\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 $deal = $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 return $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n return $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\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 (RateLimitException $e) {\n// throw $e;\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}]...
|
-7612795202019444965
|
5225837844252985444
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
3
68
3
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\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 Illuminate\Support\Facades\Redis;
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)
{
$cacheKey = $this->getRateLimitCacheKey();
$cachedRetryAfter = Redis::get($cacheKey);
if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {
throw new RateLimitException(
'Hubspot rate limit (cached circuit-breaker)',
(int) $cachedRetryAfter,
);
}
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'policy' => $this->parsePolicy($e),
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function getRateLimitCacheKey(): string
{
return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());
}
public function isHubspotRateLimit(Throwable $e): bool
{
if ($e instanceof BadRequest
|| $e instanceof DealApiException
|| $e instanceof ContactApiException
|| $e instanceof CompanyApiException
|| $e instanceof \GuzzleHttp\Exception\RequestException
) {
return (int) $e->getCode() === 429;
}
return false;
}
public function parseRetryAfter(Throwable $e): int
{
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$e ' . PHP_EOL . print_r($e, true));
return 5;
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;
}
}
$policy = $this->parsePolicy($e);
if ($policy === 'TEN_SECONDLY_ROLLING') {
return 10;
}
if ($policy === 'SECONDLY') {
return 1;
}
if ($policy === 'DAILY_LIMIT') {
return 600;
}
$this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [
'exception_class' => get_class($e),
]);
return 10;
}
public function parsePolicy(Throwable $e): ?string
{
if (! method_exists($e, 'getResponseBody')) {
return null;
}
$body = $e->getResponseBody();
if (is_string($body)) {
$body = json_decode($body, true) ?? [];
}
if (! is_array($body)) {
return null;
}
$policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;
return is_string($policy) ? strtoupper($policy) : null;
}
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
);
}
/**
* Execute a search request against HubSpot CRM objects with rate limiting.
*
* @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')
* @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.
* @return array The search response with 'results', 'total', 'paging' keys
* @throws RateLimitException When rate limit is hit
* @throws HubspotException On API errors
*/
public function search(string $objectType, array $payload): array
{
$endpoint = self::BASE_URL . "/crm/v3/objects/{$objectType}/search";
return $this->executeRequest(function () use ($endpoint, $payload) {
$response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$response ' . PHP_EOL . print_r($response, true));
return $response->toArray();
});
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$deal = $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') {
return $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
return $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
}
/**
* @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 (RateLimitException $e) {
// throw $e;
} 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);
}
}...
|
17939
|
NULL
|
NULL
|
NULL
|
|
17941
|
781
|
47
|
2026-05-11T10:42:22.929212+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778496142929_m2.jpg...
|
PhpStorm
|
faVsco.js – Client.php
|
1
|
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
|
click
|
hybrid
|
NULL
|
Project: faVsco.js, menu
PhostormVIewavigatecodeLa Project: faVsco.js, menu
PhostormVIewavigatecodeLaravelKeractorTOOISWindowFV faVsco.js°9 JY-20725-handle-HS-search-rate-linroledeyC) TrackAutomated ReportGeneratedzventonp© TrackRecordingFileSizT SyncCrmEntitiesTrait.phpCachedcrmserviceDecorator.ongoveryaeuvitycrmlaskJoo.pnp© TrackRecordingSizeEnleeneck naireT. ValidateSmitProspect:D AjReportsBadkequest.php© PaginationState.phgclass Cllent extends Baseclient 1mpLements Hubspotcllentintertace0 CalendarConference0 Crm230@ bullnornC CloseC Copper242)243244)• J crmobiectsC7 DecorateActivitv2461Dummy2481Helpersv h Hubspot2501AccountSvncStrate> Actionsa ContactsvncStraterFields• M lournal256 01 Metadatalv OpportunitySyncSt>MConcerns.(c) Hubsnotl actMoC HubspotLastMo(C) Hubsnotl actMo(C) Hubsnotl actMo(C) Hubsnotl actMo(C) HubsnotSinaleS© HubspotSyncStr© HubspotWebhoo~ M Padination© HubspotPaginat© PaginationConfi(C) PaqinationState> D ProspectSearchStr:› D Redisv D ServiceTraits() OpportunitvSvnc() SvncCrmEntitiesT SuncFieldstirait.T. WriteCrmTrait.o• M UtilsM WebhookC) BatchSvncCollectot(c) RatchSvncRedisSe(C) ClocedDea|Stadecs@ DoalFieldsService rpublic function getPaginatedDataGenerator(Nwroo&. scotal,&: $lastRecordId* Execute a search request against Hubsoot cri obnects with rate limitina.• oparam strina SobnectTupe The obnect tupe 'deals', 'compantes'.'contacts', 'calls')@param array<string, mixed> $payload The search payload with filters, sorts, properties, etc* Greturn arrou The searchresconse with "results'.total'"naonnd' keus* Athrows RateLimitExcention When rate Limit is hit* athrows HubsnotExcentionOn APt errorspublic function search(string SobjectType, array Spayload): arraySendnnint = cpf••RASF IIRIreturn $this->executeRequest(function () use ($endpoint, $payload) {Sresponse = $this->getInstance->getClient->request( method: 'POST', Sendpoint, ['json' => $payload]):Tlluminate Sunnont Sacades Loa. • channel ch'custom_channel')->info('$response' . PHP_EOL . print_r(Sresponse, return: true)):return Sresponse->toArrayO* dchrows DealAp1Exceptzon* dchrows Crmexceptionpublic function getupportun1tyBvld(strina Scrmid, array Stields: arravtry sSdeal = $this->executeRequest(fn () => Sthis->qetNewInstance()->crm()->deals@)->basicApi@->qetBvTd(Sdeal = Sthis->qetNewInstance()->crm()->deals@->basicAni(->getBvId(Scrmiid)} catch (DealAniFycention Se) $Sthis->loa-sinfortfHuhsnot Sailed to fetch onnontunitvinpason' => Se->ae+Meçcane01IOhuospot/serwice.onp" suppont Dally • In 1h 10m100% LzP• Mon 11 May 13:42:22HandleHubspotRateLimitTest v« console (PROD]* console (EUl& console (STAGINGIw.19A)PaginationConrig.pnpA3A68 V3 1= laravel.log4 SF [jiminny@localhost]& HS_local [jiminny@localhost][2026-05-07 14:21:15] local.INF0: [Hubspot] DEBUG Getting headers {"Date":["Thu,07 May 2026 14:21:15 GMT"],"Content-Type":["application/json;charset=utf-8"]."Transfer-Encoding": ["chunked"],"conneccion". Keep-alive"n"CF-Ray" : ["9f80deb8db60dc3a-SOF"],"CF-Cache-Status":"DYNAMIC"]"Strict-Transport-Secur1ty":"max-aqe=31536000* 1ncludeSubDomains: preload")accept-encoding"],"access-control-allow-credentials": "false").server-timing": ["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",cfn:desc="9-80deb8e/codc3a-IAD"""x-content-type-options": ["nosniff"]."x-hubsoot-correlation-id":"019e02d0-6fd8-7812-bdba-885b7ccb3ee3"]"Set-Cookie".[" cf bm=STUrtd0aXVrik50odaF6hZVYKhzTn0BidvMabeCtm0Y-1778163675-[IP_ADDRESS]-рT. ZaatDKxTae5zr8 2abBfWM00. ufZEXDZuHz2mBUFdzdo2aTHEs0)07-May-26 14:51:15 GMT; domain=.hubapi.com; Http0nly; Secure; SameSite=None"],"Renont-To"l."."endnoints"•!\"url\":\"https:|V/\V/a.nel.cloudflare.com/\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZLzoYdxI%2BIxVpHmsKn30%2BKVA3mFIJ2m7YRECDGS\"group\":\"cf-nel\",\"max_age\":604800}"],"success_fraction\":0.01,"report_to\":\"cf-nel\"\"max_age\":604800}"],"Server": ["cloudflare"]}} {"correlation id":"95236535-ec98-4541-b92a-adfa73b69eab"."trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}TllllW Windsurf Teams 262:129 UTF-8 P 4 spaces ®...
|
17938
|
NULL
|
NULL
|
NULL
|
|
17942
|
781
|
48
|
2026-05-11T10:42:26.228797+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778496146228_m2.jpg...
|
PhpStorm
|
faVsco.js – Client.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
3
68
3
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\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 Illuminate\Support\Facades\Redis;
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)
{
$cacheKey = $this->getRateLimitCacheKey();
$cachedRetryAfter = Redis::get($cacheKey);
if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {
throw new RateLimitException(
'Hubspot rate limit (cached circuit-breaker)',
(int) $cachedRetryAfter,
);
}
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'policy' => $this->parsePolicy($e),
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function getRateLimitCacheKey(): string
{
return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());
}
public function isHubspotRateLimit(Throwable $e): bool
{
if ($e instanceof BadRequest
|| $e instanceof DealApiException
|| $e instanceof ContactApiException
|| $e instanceof CompanyApiException
|| $e instanceof \GuzzleHttp\Exception\RequestException
) {
return (int) $e->getCode() === 429;
}
return false;
}
public function parseRetryAfter(Throwable $e): int
{
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$e ' . PHP_EOL . print_r($e, true));
return 5;
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;
}
}
$policy = $this->parsePolicy($e);
if ($policy === 'TEN_SECONDLY_ROLLING') {
return 10;
}
if ($policy === 'SECONDLY') {
return 1;
}
if ($policy === 'DAILY_LIMIT') {
return 600;
}
$this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [
'exception_class' => get_class($e),
]);
return 10;
}
public function parsePolicy(Throwable $e): ?string
{
if (! method_exists($e, 'getResponseBody')) {
return null;
}
$body = $e->getResponseBody();
if (is_string($body)) {
$body = json_decode($body, true) ?? [];
}
if (! is_array($body)) {
return null;
}
$policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;
return is_string($policy) ? strtoupper($policy) : null;
}
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
);
}
/**
* Execute a search request against HubSpot CRM objects with rate limiting.
*
* @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')
* @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.
* @return array The search response with 'results', 'total', 'paging' keys
* @throws RateLimitException When rate limit is hit
* @throws HubspotException On API errors
*/
public function search(string $objectType, array $payload): array
{
$endpoint = self::BASE_URL . "/crm/v3/objects/{$objectType}/search";
return $this->executeRequest(function () use ($endpoint, $payload) {
$response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$response ' . PHP_EOL . print_r($response, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$response ' . PHP_EOL . print_r($response, true));
return $response->toArray();
});
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$deal = $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') {
return $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
return $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
}
/**
* @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 (RateLimitException $e) {
// throw $e;
} 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);
}
}
Sync Changes...
|
[{"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":"JY-20725-handle-HS-search-rate-limit, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.09541223,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: JY-20725-handle-HS-search-rate-limit","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.82413566,"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":"HandleHubspotRateLimitTest","depth":6,"bounds":{"left":0.8394282,"top":0.019952115,"width":0.076130316,"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 'HandleHubspotRateLimitTest'","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 'HandleHubspotRateLimitTest'","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":"AXStaticText","text":"3","depth":4,"bounds":{"left":0.5515292,"top":0.15003991,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"68","depth":4,"bounds":{"left":0.56150264,"top":0.15003991,"width":0.010305851,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"3","depth":4,"bounds":{"left":0.5738032,"top":0.15003991,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.5834442,"top":0.14844373,"width":0.00731383,"height":0.018355945},"on_screen":true,"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.59075797,"top":0.14844373,"width":0.006981383,"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\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\\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 Illuminate\\Support\\Facades\\Redis;\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\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Reacts to a rate limits (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 $cacheKey = $this->getRateLimitCacheKey();\n\n $cachedRetryAfter = Redis::get($cacheKey);\n if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n (int) $cachedRetryAfter,\n );\n }\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);\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 'policy' => $this->parsePolicy($e),\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 getRateLimitCacheKey(): string\n {\n return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());\n }\n\n public function isHubspotRateLimit(Throwable $e): bool\n {\n if ($e instanceof BadRequest\n || $e instanceof DealApiException\n || $e instanceof ContactApiException\n || $e instanceof CompanyApiException\n || $e instanceof \\GuzzleHttp\\Exception\\RequestException\n ) {\n return (int) $e->getCode() === 429;\n }\n\n return false;\n }\n\n public function parseRetryAfter(Throwable $e): int\n {\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$e ' . PHP_EOL . print_r($e, true));\n return 5;\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 $policy = $this->parsePolicy($e);\n if ($policy === 'TEN_SECONDLY_ROLLING') {\n return 10;\n }\n if ($policy === 'SECONDLY') {\n return 1;\n }\n if ($policy === 'DAILY_LIMIT') {\n return 600;\n }\n\n $this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [\n 'exception_class' => get_class($e),\n ]);\n\n return 10;\n }\n\n public function parsePolicy(Throwable $e): ?string\n {\n if (! method_exists($e, 'getResponseBody')) {\n return null;\n }\n\n $body = $e->getResponseBody();\n if (is_string($body)) {\n $body = json_decode($body, true) ?? [];\n }\n\n if (! is_array($body)) {\n return null;\n }\n\n $policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;\n\n return is_string($policy) ? strtoupper($policy) : null;\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 * Execute a search request against HubSpot CRM objects with rate limiting.\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n * @return array The search response with 'results', 'total', 'paging' keys\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n */\n public function search(string $objectType, array $payload): array\n {\n $endpoint = self::BASE_URL . \"/crm/v3/objects/{$objectType}/search\";\n\n return $this->executeRequest(function () use ($endpoint, $payload) {\n $response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$response ' . PHP_EOL . print_r($response, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$response ' . PHP_EOL . print_r($response, true));\n\n return $response->toArray();\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 $deal = $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 return $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n return $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\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 (RateLimitException $e) {\n// throw $e;\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\\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 Illuminate\\Support\\Facades\\Redis;\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\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Reacts to a rate limits (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 $cacheKey = $this->getRateLimitCacheKey();\n\n $cachedRetryAfter = Redis::get($cacheKey);\n if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n (int) $cachedRetryAfter,\n );\n }\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);\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 'policy' => $this->parsePolicy($e),\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 getRateLimitCacheKey(): string\n {\n return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());\n }\n\n public function isHubspotRateLimit(Throwable $e): bool\n {\n if ($e instanceof BadRequest\n || $e instanceof DealApiException\n || $e instanceof ContactApiException\n || $e instanceof CompanyApiException\n || $e instanceof \\GuzzleHttp\\Exception\\RequestException\n ) {\n return (int) $e->getCode() === 429;\n }\n\n return false;\n }\n\n public function parseRetryAfter(Throwable $e): int\n {\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$e ' . PHP_EOL . print_r($e, true));\n return 5;\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 $policy = $this->parsePolicy($e);\n if ($policy === 'TEN_SECONDLY_ROLLING') {\n return 10;\n }\n if ($policy === 'SECONDLY') {\n return 1;\n }\n if ($policy === 'DAILY_LIMIT') {\n return 600;\n }\n\n $this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [\n 'exception_class' => get_class($e),\n ]);\n\n return 10;\n }\n\n public function parsePolicy(Throwable $e): ?string\n {\n if (! method_exists($e, 'getResponseBody')) {\n return null;\n }\n\n $body = $e->getResponseBody();\n if (is_string($body)) {\n $body = json_decode($body, true) ?? [];\n }\n\n if (! is_array($body)) {\n return null;\n }\n\n $policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;\n\n return is_string($policy) ? strtoupper($policy) : null;\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 * Execute a search request against HubSpot CRM objects with rate limiting.\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n * @return array The search response with 'results', 'total', 'paging' keys\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n */\n public function search(string $objectType, array $payload): array\n {\n $endpoint = self::BASE_URL . \"/crm/v3/objects/{$objectType}/search\";\n\n return $this->executeRequest(function () use ($endpoint, $payload) {\n $response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$response ' . PHP_EOL . print_r($response, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$response ' . PHP_EOL . print_r($response, true));\n\n return $response->toArray();\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 $deal = $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 return $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n return $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\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 (RateLimitException $e) {\n// throw $e;\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":"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}]...
|
8856636435912162611
|
5225837844252985444
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
3
68
3
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\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 Illuminate\Support\Facades\Redis;
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)
{
$cacheKey = $this->getRateLimitCacheKey();
$cachedRetryAfter = Redis::get($cacheKey);
if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {
throw new RateLimitException(
'Hubspot rate limit (cached circuit-breaker)',
(int) $cachedRetryAfter,
);
}
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'policy' => $this->parsePolicy($e),
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function getRateLimitCacheKey(): string
{
return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());
}
public function isHubspotRateLimit(Throwable $e): bool
{
if ($e instanceof BadRequest
|| $e instanceof DealApiException
|| $e instanceof ContactApiException
|| $e instanceof CompanyApiException
|| $e instanceof \GuzzleHttp\Exception\RequestException
) {
return (int) $e->getCode() === 429;
}
return false;
}
public function parseRetryAfter(Throwable $e): int
{
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$e ' . PHP_EOL . print_r($e, true));
return 5;
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;
}
}
$policy = $this->parsePolicy($e);
if ($policy === 'TEN_SECONDLY_ROLLING') {
return 10;
}
if ($policy === 'SECONDLY') {
return 1;
}
if ($policy === 'DAILY_LIMIT') {
return 600;
}
$this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [
'exception_class' => get_class($e),
]);
return 10;
}
public function parsePolicy(Throwable $e): ?string
{
if (! method_exists($e, 'getResponseBody')) {
return null;
}
$body = $e->getResponseBody();
if (is_string($body)) {
$body = json_decode($body, true) ?? [];
}
if (! is_array($body)) {
return null;
}
$policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;
return is_string($policy) ? strtoupper($policy) : null;
}
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
);
}
/**
* Execute a search request against HubSpot CRM objects with rate limiting.
*
* @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')
* @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.
* @return array The search response with 'results', 'total', 'paging' keys
* @throws RateLimitException When rate limit is hit
* @throws HubspotException On API errors
*/
public function search(string $objectType, array $payload): array
{
$endpoint = self::BASE_URL . "/crm/v3/objects/{$objectType}/search";
return $this->executeRequest(function () use ($endpoint, $payload) {
$response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$response ' . PHP_EOL . print_r($response, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$response ' . PHP_EOL . print_r($response, true));
return $response->toArray();
});
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$deal = $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') {
return $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
return $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
}
/**
* @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 (RateLimitException $e) {
// throw $e;
} 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);
}
}
Sync Changes...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
17943
|
780
|
50
|
2026-05-11T10:42:27.440730+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778496147440_m1.jpg...
|
PhpStorm
|
faVsco.js – Client.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
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":"JY-20725-handle-HS-search-rate-limit, menu","depth":5,"on_screen":true,"help_text":"Git Branch: JY-20725-handle-HS-search-rate-limit","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":"HandleHubspotRateLimitTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'HandleHubspotRateLimitTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'HandleHubspotRateLimitTest'","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}]...
|
1579713784452094674
|
-8780890023316608054
|
click
|
hybrid
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
SlackFileEditViewGoHistoryWindowHelpDOCKERO ₴1DEV (docker)Last login: Sun May 10 21:10:26 on ttys013₴2APP (-zsh)883Poetry could not find a pyproject.toml file in /Users/lukas or its parentsPoetry could not find a pyproject.toml file in /Users/lukas or its parentslukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ 0-zshHomeDMsActivityFilesLater..•More(allSupport Daily • in 1h 18 m100% <78• Mon 11 May 13:42:27ED→Describe what you are looking forJiminny ...Toast# conrusion-clinic# curiosity_lab# engineering# general# jiminny-bg# platform-tickets# product_launches# random# releases# sofia-office# support# thank-yous# the_people_of jimi...HomeMessages^ Direct messages®. Aneliya Angelova®. Galya Dimitrova& Petko Kashinski&. Stefka StoyanovaVasil Vasilev el&. Nikolay IvanovAneliya Angelova, ...Stoyan Tanev• VesE Lukas Kovalik y... 0AboutiminnyFriday, May 8th ~Added by Toast for GitHubToday ~Toast APP10:00 AMReviewapp#12059 Jy 20820 es reindexstream model hydration2 days old • 12 files changed •@Vasil VasilevAdded by Toast for GitHubResolve Conflictsapp#11443 Test hublets latency5 months old • 20 files changed#11327 JY-19501 webhookbased opportunity syncShow moreAdded by Toast for GitHubNeeds Loveapp#12024 JY-20773 fix user pilottracking ofr automated reportgenerated12 days old • 1 file changed::: AppsS Jira CloudMessage ToastToast+AaGanala CalaNew...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
17944
|
781
|
49
|
2026-05-11T10:42:27.484747+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778496147484_m2.jpg...
|
PhpStorm
|
faVsco.js – Client.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings...
|
[{"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":"JY-20725-handle-HS-search-rate-limit, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.09541223,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: JY-20725-handle-HS-search-rate-limit","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.82413566,"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":"HandleHubspotRateLimitTest","depth":6,"bounds":{"left":0.8394282,"top":0.019952115,"width":0.076130316,"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 'HandleHubspotRateLimitTest'","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 'HandleHubspotRateLimitTest'","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}]...
|
1579713784452094674
|
-8780890023316608054
|
click
|
hybrid
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
PhostormVIewINavicarecodeLaravelKeractorTOOISWindowFV faVsco.js°9 JY-20725-handle-HS-search-rate-Iiyroledey© TrackRecordingFileSizT SyncCrmEntitiesTrait.phpCachedcrmserviceDecorator.ongoveryaeuvitycrmlaskJoo.pnp© TrackRecordingSizeEnT. ValidateSmitProspect:leenecknaik© Job.phpD AjReportsBadkeouest.php© PaginationState.phgclass Cllent extends Baseclient 1mpLements Hubspotcllentintertace0 CalendarConference0 Crm@ bullnornC CloseC Copper230242)243244)>J Crmobiects_ DecorareAcuivilyDummy24612472481Helpersv h Hubspot2501AccountSvncStrate>D Actionsa ContactsvncStraterFields• M lournal256 C1 Metadatalv OpportunitySyncSt> O Concerns(c) Hubsnotl actMoC HubspotLastMo(C) Hubsnotl actMo© HubspotLastMo(C) Hubsnotl actMo(C) HubsnotSinaleS© HubspotSyncStr© HubspotWebhoo~ M Padination© HubspotPaginat© PaginationConfi(C) PaqinationState> D ProspectSearchStr:› D Redisv D ServiceTraits() OpportunitvSvnc() SvncCrmEntitiesT SuncFieldstirait.T. WriteCrmTrait.o• M UtilsM WebhookC) BatchSvncCollectot(c) RatchSvncRedisSeClient nhr(C) ClocedDea|Stadecc@ DoalFieldsService rpublic function getPaginatedDataGenerator(Nwroo&. scotal,&: $lastRecordId* Execute a search request against Hubsoot cri obnects with rate limitina.* aparam strina Sobnecupe The obnect tupe deals', 'compannes''contacts', 'calls')@param array<string, mixed> $payload The search payload with filters, sorts, properties, etc* Greturn arrou The searchresconse with'total', "naonno'keus* Athrows RateLimitExcention When rate Limit is hit* athrows HubsnotExcentionOn APt errorspublic function search(string SobjectType, array Spayload): arraySendnnint = cpf••RASF IIRIneturn_Sthis->pyecuteRenuest/functionO uce (Sendnoint Snavloadh&Sresponse = $this->getInstance->getClient->request(rhod: 'POST', Sendpoint, ['json' => $payload]):Tlluminate Sunnont Facades Loa. • channelà cha'custom_channel')->info('$response' . PHP_EOL . print_r(Sresponse,return: true));(Illuminate(Support \Facades\Log::channel('custom_channel')->info('$response' . PHP_EOL . print_r(Sresponse, true)):return Sresponse->toArrayO:})H* dchrows DealAp1Exceptzon* dchrows Crmexceptionpublic function getupportun1tyßyld(string Scrmid, array Stields: arravtrySdeal = Sthis-teRequest(fn () => Sthis->qetNewInstance()->crm()->deals@)->basicApi@->qetBvTd(Sdeal = Sthis->aetNewinstanceO->crmO->dealsO->basicAnio->netBvideimolode( separator:" ' Sfields)} catch (NealAniFycention Se) $Sthis-loa-sinforlHuhsnot Sailed to fetch onnontunitvi'crm_id' => $crmId,Se->ae+Meçcane O(c) HubSpot/Service.onp" suppont Dally • In 1h 10m100% LzP• Mon 11 May 13:42:26HandleHubspotRateLimitTest v« console (PROD]* console (EUl& console [STAGINGIw.19A)PaginationConrig.pnpA3A68 V3 1= laravel.log4 SF [jiminny@localhost]& HS_local [jiminny@localhost][2026-05-07 14:21:15] local.INF0: [Hubspot] DEBUG Getting headers {"Date":["Thu,07 May 2026 14:21:15 GMT"],"Content-Type":["application/json;charset=utf-8"]."Transfer-Encoding": ["chunked"],"conneccion". Keep-alive"n"CF-Ray": ["9f80deb8db60dc3a-SOF"],"CF-Cache-Status":"DYNAMIC"]"Strict-Transport-Secur1ty":"max-aqe=31536000* 1ncludeSubDomains: preload")accept-encoding"],"access-control-allow-credentials": "false")."server-timing": ["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",cfn:desc="9-80de8ercodcSa-TAD"'"x-content-type-options": ["nosniff"]."x-hubsoot-correlation-id":"019e02d0-6fd8-7812-bdba-885b7ccb3ee3"])"Set-Cookie".[" cf bm=STUrtd0aXVrik50odaF6hZVYKhzTn0BidvMabeCtm0Y-1778163675-[IP_ADDRESS]-рT. ZaatDKxTae5zr8 2abBfWM00. ufZEXDZuHz2mBUFdzdo2aTHEs0)07-May-26 14:51:15 GMT; domain=.hubapi.com; Http0nly; Secure; SameSite=None"],"Renont-To"l."."endnoints"•!\"url\":\"https:|V/\V/a.nel.cloudflare.com/\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZLzoYdxI%2BIxVpHmsKn30%2BKVA3mFIJ2m7YRECDGS\"group\":\"cf-nel\",\"max_age\":604800}"],"success_fraction\":0.01,"report_to\":\"cf-nel\"\"max_age\":604800}"],"Server": ["cloudflare"]}} {"correlation id":"95236535-ec98-4541-b92a-adfa73b69eab"."trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}miltW Windsurf Teams 263:129 UTF-8 f 4 spaces ®...
|
17942
|
NULL
|
NULL
|
NULL
|
|
17945
|
781
|
50
|
2026-05-11T10:42:30.638009+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778496150638_m2.jpg...
|
PhpStorm
|
faVsco.js – Client.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
3
68
3
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\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 Illuminate\Support\Facades\Redis;
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)
{
$cacheKey = $this->getRateLimitCacheKey();
$cachedRetryAfter = Redis::get($cacheKey);
if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {
throw new RateLimitException(
'Hubspot rate limit (cached circuit-breaker)',
(int) $cachedRetryAfter,
);
}
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'policy' => $this->parsePolicy($e),
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function getRateLimitCacheKey(): string
{
return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());
}
public function isHubspotRateLimit(Throwable $e): bool
{
if ($e instanceof BadRequest
|| $e instanceof DealApiException
|| $e instanceof ContactApiException
|| $e instanceof CompanyApiException
|| $e instanceof \GuzzleHttp\Exception\RequestException
) {
return (int) $e->getCode() === 429;
}
return false;
}
public function parseRetryAfter(Throwable $e): int
{
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$e ' . PHP_EOL . print_r($e, true));
return 5;
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;
}
}
$policy = $this->parsePolicy($e);
if ($policy === 'TEN_SECONDLY_ROLLING') {
return 10;
}
if ($policy === 'SECONDLY') {
return 1;
}
if ($policy === 'DAILY_LIMIT') {
return 600;
}
$this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [
'exception_class' => get_class($e),
]);
return 10;
}
public function parsePolicy(Throwable $e): ?string
{
if (! method_exists($e, 'getResponseBody')) {
return null;
}
$body = $e->getResponseBody();
if (is_string($body)) {
$body = json_decode($body, true) ?? [];
}
if (! is_array($body)) {
return null;
}
$policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;
return is_string($policy) ? strtoupper($policy) : null;
}
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
);
}
/**
* Execute a search request against HubSpot CRM objects with rate limiting.
*
* @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')
* @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.
* @return array The search response with 'results', 'total', 'paging' keys
* @throws RateLimitException When rate limit is hit
* @throws HubspotException On API errors
*/
public function search(string $objectType, array $payload): array
{
$endpoint = self::BASE_URL . "/crm/v3/objects/{$objectType}/search";
return $this->executeRequest(function () use ($endpoint, $payload) {
$response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$response ' . PHP_EOL . print_r($response, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$response ' . PHP_EOL . print_r($response->ge, true));
return $response->toArray();
});
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$deal = $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') {
return $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
return $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
}
/**
* @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 (RateLimitException $e) {
// throw $e;
} 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);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error...
|
[{"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":"JY-20725-handle-HS-search-rate-limit, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.09541223,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: JY-20725-handle-HS-search-rate-limit","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.82413566,"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":"HandleHubspotRateLimitTest","depth":6,"bounds":{"left":0.8394282,"top":0.019952115,"width":0.076130316,"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 'HandleHubspotRateLimitTest'","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 'HandleHubspotRateLimitTest'","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":"AXStaticText","text":"3","depth":4,"bounds":{"left":0.5515292,"top":0.15003991,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"68","depth":4,"bounds":{"left":0.56150264,"top":0.15003991,"width":0.010305851,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"3","depth":4,"bounds":{"left":0.5738032,"top":0.15003991,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.5834442,"top":0.14844373,"width":0.00731383,"height":0.018355945},"on_screen":true,"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.59075797,"top":0.14844373,"width":0.006981383,"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\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\\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 Illuminate\\Support\\Facades\\Redis;\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\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Reacts to a rate limits (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 $cacheKey = $this->getRateLimitCacheKey();\n\n $cachedRetryAfter = Redis::get($cacheKey);\n if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n (int) $cachedRetryAfter,\n );\n }\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);\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 'policy' => $this->parsePolicy($e),\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 getRateLimitCacheKey(): string\n {\n return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());\n }\n\n public function isHubspotRateLimit(Throwable $e): bool\n {\n if ($e instanceof BadRequest\n || $e instanceof DealApiException\n || $e instanceof ContactApiException\n || $e instanceof CompanyApiException\n || $e instanceof \\GuzzleHttp\\Exception\\RequestException\n ) {\n return (int) $e->getCode() === 429;\n }\n\n return false;\n }\n\n public function parseRetryAfter(Throwable $e): int\n {\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$e ' . PHP_EOL . print_r($e, true));\n return 5;\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 $policy = $this->parsePolicy($e);\n if ($policy === 'TEN_SECONDLY_ROLLING') {\n return 10;\n }\n if ($policy === 'SECONDLY') {\n return 1;\n }\n if ($policy === 'DAILY_LIMIT') {\n return 600;\n }\n\n $this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [\n 'exception_class' => get_class($e),\n ]);\n\n return 10;\n }\n\n public function parsePolicy(Throwable $e): ?string\n {\n if (! method_exists($e, 'getResponseBody')) {\n return null;\n }\n\n $body = $e->getResponseBody();\n if (is_string($body)) {\n $body = json_decode($body, true) ?? [];\n }\n\n if (! is_array($body)) {\n return null;\n }\n\n $policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;\n\n return is_string($policy) ? strtoupper($policy) : null;\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 * Execute a search request against HubSpot CRM objects with rate limiting.\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n * @return array The search response with 'results', 'total', 'paging' keys\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n */\n public function search(string $objectType, array $payload): array\n {\n $endpoint = self::BASE_URL . \"/crm/v3/objects/{$objectType}/search\";\n\n return $this->executeRequest(function () use ($endpoint, $payload) {\n $response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$response ' . PHP_EOL . print_r($response, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$response ' . PHP_EOL . print_r($response->ge, true));\n\n return $response->toArray();\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 $deal = $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 return $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n return $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\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 (RateLimitException $e) {\n// throw $e;\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\\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 Illuminate\\Support\\Facades\\Redis;\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\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Reacts to a rate limits (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 $cacheKey = $this->getRateLimitCacheKey();\n\n $cachedRetryAfter = Redis::get($cacheKey);\n if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n (int) $cachedRetryAfter,\n );\n }\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);\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 'policy' => $this->parsePolicy($e),\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 getRateLimitCacheKey(): string\n {\n return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());\n }\n\n public function isHubspotRateLimit(Throwable $e): bool\n {\n if ($e instanceof BadRequest\n || $e instanceof DealApiException\n || $e instanceof ContactApiException\n || $e instanceof CompanyApiException\n || $e instanceof \\GuzzleHttp\\Exception\\RequestException\n ) {\n return (int) $e->getCode() === 429;\n }\n\n return false;\n }\n\n public function parseRetryAfter(Throwable $e): int\n {\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$e ' . PHP_EOL . print_r($e, true));\n return 5;\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 $policy = $this->parsePolicy($e);\n if ($policy === 'TEN_SECONDLY_ROLLING') {\n return 10;\n }\n if ($policy === 'SECONDLY') {\n return 1;\n }\n if ($policy === 'DAILY_LIMIT') {\n return 600;\n }\n\n $this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [\n 'exception_class' => get_class($e),\n ]);\n\n return 10;\n }\n\n public function parsePolicy(Throwable $e): ?string\n {\n if (! method_exists($e, 'getResponseBody')) {\n return null;\n }\n\n $body = $e->getResponseBody();\n if (is_string($body)) {\n $body = json_decode($body, true) ?? [];\n }\n\n if (! is_array($body)) {\n return null;\n }\n\n $policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;\n\n return is_string($policy) ? strtoupper($policy) : null;\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 * Execute a search request against HubSpot CRM objects with rate limiting.\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n * @return array The search response with 'results', 'total', 'paging' keys\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n */\n public function search(string $objectType, array $payload): array\n {\n $endpoint = self::BASE_URL . \"/crm/v3/objects/{$objectType}/search\";\n\n return $this->executeRequest(function () use ($endpoint, $payload) {\n $response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$response ' . PHP_EOL . print_r($response, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$response ' . PHP_EOL . print_r($response->ge, true));\n\n return $response->toArray();\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 $deal = $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 return $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n return $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\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 (RateLimitException $e) {\n// throw $e;\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":"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":"19","depth":4,"bounds":{"left":0.96276593,"top":0.07581804,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.9740692,"top":0.074221864,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-7885641143347525917
|
5225837844252985444
|
visual_change
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
3
68
3
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\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 Illuminate\Support\Facades\Redis;
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)
{
$cacheKey = $this->getRateLimitCacheKey();
$cachedRetryAfter = Redis::get($cacheKey);
if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {
throw new RateLimitException(
'Hubspot rate limit (cached circuit-breaker)',
(int) $cachedRetryAfter,
);
}
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'policy' => $this->parsePolicy($e),
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function getRateLimitCacheKey(): string
{
return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());
}
public function isHubspotRateLimit(Throwable $e): bool
{
if ($e instanceof BadRequest
|| $e instanceof DealApiException
|| $e instanceof ContactApiException
|| $e instanceof CompanyApiException
|| $e instanceof \GuzzleHttp\Exception\RequestException
) {
return (int) $e->getCode() === 429;
}
return false;
}
public function parseRetryAfter(Throwable $e): int
{
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$e ' . PHP_EOL . print_r($e, true));
return 5;
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;
}
}
$policy = $this->parsePolicy($e);
if ($policy === 'TEN_SECONDLY_ROLLING') {
return 10;
}
if ($policy === 'SECONDLY') {
return 1;
}
if ($policy === 'DAILY_LIMIT') {
return 600;
}
$this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [
'exception_class' => get_class($e),
]);
return 10;
}
public function parsePolicy(Throwable $e): ?string
{
if (! method_exists($e, 'getResponseBody')) {
return null;
}
$body = $e->getResponseBody();
if (is_string($body)) {
$body = json_decode($body, true) ?? [];
}
if (! is_array($body)) {
return null;
}
$policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;
return is_string($policy) ? strtoupper($policy) : null;
}
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
);
}
/**
* Execute a search request against HubSpot CRM objects with rate limiting.
*
* @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')
* @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.
* @return array The search response with 'results', 'total', 'paging' keys
* @throws RateLimitException When rate limit is hit
* @throws HubspotException On API errors
*/
public function search(string $objectType, array $payload): array
{
$endpoint = self::BASE_URL . "/crm/v3/objects/{$objectType}/search";
return $this->executeRequest(function () use ($endpoint, $payload) {
$response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$response ' . PHP_EOL . print_r($response, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$response ' . PHP_EOL . print_r($response->ge, true));
return $response->toArray();
});
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$deal = $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') {
return $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
return $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
}
/**
* @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 (RateLimitException $e) {
// throw $e;
} 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);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
17946
|
781
|
51
|
2026-05-11T10:42:36.608177+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778496156608_m2.jpg...
|
PhpStorm
|
faVsco.js – Client.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
3
68
3
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\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 Illuminate\Support\Facades\Redis;
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)
{
$cacheKey = $this->getRateLimitCacheKey();
$cachedRetryAfter = Redis::get($cacheKey);
if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {
throw new RateLimitException(
'Hubspot rate limit (cached circuit-breaker)',
(int) $cachedRetryAfter,
);
}
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'policy' => $this->parsePolicy($e),
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function getRateLimitCacheKey(): string
{
return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());
}
public function isHubspotRateLimit(Throwable $e): bool
{
if ($e instanceof BadRequest
|| $e instanceof DealApiException
|| $e instanceof ContactApiException
|| $e instanceof CompanyApiException
|| $e instanceof \GuzzleHttp\Exception\RequestException
) {
return (int) $e->getCode() === 429;
}
return false;
}
public function parseRetryAfter(Throwable $e): int
{
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$e ' . PHP_EOL . print_r($e, true));
return 5;
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;
}
}
$policy = $this->parsePolicy($e);
if ($policy === 'TEN_SECONDLY_ROLLING') {
return 10;
}
if ($policy === 'SECONDLY') {
return 1;
}
if ($policy === 'DAILY_LIMIT') {
return 600;
}
$this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [
'exception_class' => get_class($e),
]);
return 10;
}
public function parsePolicy(Throwable $e): ?string
{
if (! method_exists($e, 'getResponseBody')) {
return null;
}
$body = $e->getResponseBody();
if (is_string($body)) {
$body = json_decode($body, true) ?? [];
}
if (! is_array($body)) {
return null;
}
$policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;
return is_string($policy) ? strtoupper($policy) : null;
}
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
);
}
/**
* Execute a search request against HubSpot CRM objects with rate limiting.
*
* @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')
* @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.
* @return array The search response with 'results', 'total', 'paging' keys
* @throws RateLimitException When rate limit is hit
* @throws HubspotException On API errors
*/
public function search(string $objectType, array $payload): array
{
$endpoint = self::BASE_URL . "/crm/v3/objects/{$objectType}/search";
return $this->executeRequest(function () use ($endpoint, $payload) {
$response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$response ' . PHP_EOL . print_r($response, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$response ' . PHP_EOL . print_r($response->getResponseHeaders(), true));
return $response->toArray();
});
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$deal = $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') {
return $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
return $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
}
/**
* @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 (RateLimitException $e) {
// throw $e;
} 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);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
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":"JY-20725-handle-HS-search-rate-limit, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.09541223,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: JY-20725-handle-HS-search-rate-limit","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.82413566,"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":"HandleHubspotRateLimitTest","depth":6,"bounds":{"left":0.8394282,"top":0.019952115,"width":0.076130316,"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 'HandleHubspotRateLimitTest'","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 'HandleHubspotRateLimitTest'","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":"AXStaticText","text":"3","depth":4,"bounds":{"left":0.5515292,"top":0.15003991,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"68","depth":4,"bounds":{"left":0.56150264,"top":0.15003991,"width":0.010305851,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"3","depth":4,"bounds":{"left":0.5738032,"top":0.15003991,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.5834442,"top":0.14844373,"width":0.00731383,"height":0.018355945},"on_screen":true,"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.59075797,"top":0.14844373,"width":0.006981383,"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\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\\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 Illuminate\\Support\\Facades\\Redis;\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\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Reacts to a rate limits (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 $cacheKey = $this->getRateLimitCacheKey();\n\n $cachedRetryAfter = Redis::get($cacheKey);\n if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n (int) $cachedRetryAfter,\n );\n }\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);\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 'policy' => $this->parsePolicy($e),\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 getRateLimitCacheKey(): string\n {\n return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());\n }\n\n public function isHubspotRateLimit(Throwable $e): bool\n {\n if ($e instanceof BadRequest\n || $e instanceof DealApiException\n || $e instanceof ContactApiException\n || $e instanceof CompanyApiException\n || $e instanceof \\GuzzleHttp\\Exception\\RequestException\n ) {\n return (int) $e->getCode() === 429;\n }\n\n return false;\n }\n\n public function parseRetryAfter(Throwable $e): int\n {\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$e ' . PHP_EOL . print_r($e, true));\n return 5;\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 $policy = $this->parsePolicy($e);\n if ($policy === 'TEN_SECONDLY_ROLLING') {\n return 10;\n }\n if ($policy === 'SECONDLY') {\n return 1;\n }\n if ($policy === 'DAILY_LIMIT') {\n return 600;\n }\n\n $this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [\n 'exception_class' => get_class($e),\n ]);\n\n return 10;\n }\n\n public function parsePolicy(Throwable $e): ?string\n {\n if (! method_exists($e, 'getResponseBody')) {\n return null;\n }\n\n $body = $e->getResponseBody();\n if (is_string($body)) {\n $body = json_decode($body, true) ?? [];\n }\n\n if (! is_array($body)) {\n return null;\n }\n\n $policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;\n\n return is_string($policy) ? strtoupper($policy) : null;\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 * Execute a search request against HubSpot CRM objects with rate limiting.\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n * @return array The search response with 'results', 'total', 'paging' keys\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n */\n public function search(string $objectType, array $payload): array\n {\n $endpoint = self::BASE_URL . \"/crm/v3/objects/{$objectType}/search\";\n\n return $this->executeRequest(function () use ($endpoint, $payload) {\n $response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$response ' . PHP_EOL . print_r($response, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$response ' . PHP_EOL . print_r($response->getResponseHeaders(), true));\n\n return $response->toArray();\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 $deal = $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 return $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n return $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\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 (RateLimitException $e) {\n// throw $e;\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\\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 Illuminate\\Support\\Facades\\Redis;\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\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Reacts to a rate limits (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 $cacheKey = $this->getRateLimitCacheKey();\n\n $cachedRetryAfter = Redis::get($cacheKey);\n if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n (int) $cachedRetryAfter,\n );\n }\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);\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 'policy' => $this->parsePolicy($e),\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 getRateLimitCacheKey(): string\n {\n return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());\n }\n\n public function isHubspotRateLimit(Throwable $e): bool\n {\n if ($e instanceof BadRequest\n || $e instanceof DealApiException\n || $e instanceof ContactApiException\n || $e instanceof CompanyApiException\n || $e instanceof \\GuzzleHttp\\Exception\\RequestException\n ) {\n return (int) $e->getCode() === 429;\n }\n\n return false;\n }\n\n public function parseRetryAfter(Throwable $e): int\n {\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$e ' . PHP_EOL . print_r($e, true));\n return 5;\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 $policy = $this->parsePolicy($e);\n if ($policy === 'TEN_SECONDLY_ROLLING') {\n return 10;\n }\n if ($policy === 'SECONDLY') {\n return 1;\n }\n if ($policy === 'DAILY_LIMIT') {\n return 600;\n }\n\n $this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [\n 'exception_class' => get_class($e),\n ]);\n\n return 10;\n }\n\n public function parsePolicy(Throwable $e): ?string\n {\n if (! method_exists($e, 'getResponseBody')) {\n return null;\n }\n\n $body = $e->getResponseBody();\n if (is_string($body)) {\n $body = json_decode($body, true) ?? [];\n }\n\n if (! is_array($body)) {\n return null;\n }\n\n $policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;\n\n return is_string($policy) ? strtoupper($policy) : null;\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 * Execute a search request against HubSpot CRM objects with rate limiting.\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n * @return array The search response with 'results', 'total', 'paging' keys\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n */\n public function search(string $objectType, array $payload): array\n {\n $endpoint = self::BASE_URL . \"/crm/v3/objects/{$objectType}/search\";\n\n return $this->executeRequest(function () use ($endpoint, $payload) {\n $response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$response ' . PHP_EOL . print_r($response, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$response ' . PHP_EOL . print_r($response->getResponseHeaders(), true));\n\n return $response->toArray();\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 $deal = $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 return $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n return $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\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 (RateLimitException $e) {\n// throw $e;\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":"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":"19","depth":4,"bounds":{"left":0.96276593,"top":0.07581804,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.9740692,"top":0.074221864,"width":0.00731383,"height":0.018355945},"on_screen":true,"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.98138297,"top":0.074221864,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","depth":4,"bounds":{"left":0.61702126,"top":0.0726257,"width":0.38297874,"height":0.9066241},"on_screen":true,"lines":[{"char_start":273,"char_count":32,"bounds":{"left":0.61702126,"top":0.0,"width":0.080119684,"height":0.014365523}},{"char_start":305,"char_count":79,"bounds":{"left":0.61702126,"top":0.0,"width":0.20212767,"height":0.014365523}},{"char_start":384,"char_count":18,"bounds":{"left":0.61702126,"top":0.0,"width":0.043882977,"height":0.014365523}},{"char_start":402,"char_count":21,"bounds":{"left":0.61702126,"top":0.0,"width":0.051861703,"height":0.014365523}},{"char_start":423,"char_count":48,"bounds":{"left":0.61702126,"top":0.0,"width":0.12167553,"height":0.014365523}},{"char_start":471,"char_count":72,"bounds":{"left":0.61702126,"top":0.0015961692,"width":0.18384309,"height":0.014365523}},{"char_start":543,"char_count":40,"bounds":{"left":0.61702126,"top":0.01915403,"width":0.10106383,"height":0.014365523}},{"char_start":583,"char_count":41,"bounds":{"left":0.61702126,"top":0.03671189,"width":0.10372341,"height":0.014365523}},{"char_start":624,"char_count":72,"bounds":{"left":0.61702126,"top":0.054269753,"width":0.18384309,"height":0.014365523}},{"char_start":696,"char_count":219,"bounds":{"left":0.61702126,"top":0.07182761,"width":0.38297874,"height":0.014365523}},{"char_start":915,"char_count":83,"bounds":{"left":0.61702126,"top":0.08938547,"width":0.21243352,"height":0.014365523}},{"char_start":998,"char_count":20,"bounds":{"left":0.61702126,"top":0.10694334,"width":0.04920213,"height":0.014365523}},{"char_start":1018,"char_count":17,"bounds":{"left":0.61702126,"top":0.1245012,"width":0.041223403,"height":0.014365523}},{"char_start":1035,"char_count":203,"bounds":{"left":0.61702126,"top":0.14205906,"width":0.38297874,"height":0.014365523}},{"char_start":1238,"char_count":22,"bounds":{"left":0.61702126,"top":0.15961692,"width":0.05418883,"height":0.014365523}},{"char_start":1260,"char_count":23,"bounds":{"left":0.61702126,"top":0.17717478,"width":0.056848403,"height":0.014365523}},{"char_start":1283,"char_count":10,"bounds":{"left":0.61702126,"top":0.19473264,"width":0.023271276,"height":0.014365523}},{"char_start":1293,"char_count":27,"bounds":{"left":0.61702126,"top":0.2122905,"width":0.06715426,"height":0.014365523}},{"char_start":1320,"char_count":26,"bounds":{"left":0.61702126,"top":0.22984837,"width":0.06482713,"height":0.014365523}},{"char_start":1346,"char_count":23,"bounds":{"left":0.61702126,"top":0.24740623,"width":0.056848403,"height":0.014365523}},{"char_start":1369,"char_count":28,"bounds":{"left":0.61702126,"top":0.26496407,"width":0.06981383,"height":0.014365523}},{"char_start":1397,"char_count":57,"bounds":{"left":0.61702126,"top":0.28252193,"width":0.14494681,"height":0.014365523}}],"value":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","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}]...
|
8673868863936031688
|
6378618611371477348
|
visual_change
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
3
68
3
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\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 Illuminate\Support\Facades\Redis;
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)
{
$cacheKey = $this->getRateLimitCacheKey();
$cachedRetryAfter = Redis::get($cacheKey);
if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {
throw new RateLimitException(
'Hubspot rate limit (cached circuit-breaker)',
(int) $cachedRetryAfter,
);
}
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'policy' => $this->parsePolicy($e),
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function getRateLimitCacheKey(): string
{
return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());
}
public function isHubspotRateLimit(Throwable $e): bool
{
if ($e instanceof BadRequest
|| $e instanceof DealApiException
|| $e instanceof ContactApiException
|| $e instanceof CompanyApiException
|| $e instanceof \GuzzleHttp\Exception\RequestException
) {
return (int) $e->getCode() === 429;
}
return false;
}
public function parseRetryAfter(Throwable $e): int
{
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$e ' . PHP_EOL . print_r($e, true));
return 5;
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;
}
}
$policy = $this->parsePolicy($e);
if ($policy === 'TEN_SECONDLY_ROLLING') {
return 10;
}
if ($policy === 'SECONDLY') {
return 1;
}
if ($policy === 'DAILY_LIMIT') {
return 600;
}
$this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [
'exception_class' => get_class($e),
]);
return 10;
}
public function parsePolicy(Throwable $e): ?string
{
if (! method_exists($e, 'getResponseBody')) {
return null;
}
$body = $e->getResponseBody();
if (is_string($body)) {
$body = json_decode($body, true) ?? [];
}
if (! is_array($body)) {
return null;
}
$policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;
return is_string($policy) ? strtoupper($policy) : null;
}
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
);
}
/**
* Execute a search request against HubSpot CRM objects with rate limiting.
*
* @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')
* @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.
* @return array The search response with 'results', 'total', 'paging' keys
* @throws RateLimitException When rate limit is hit
* @throws HubspotException On API errors
*/
public function search(string $objectType, array $payload): array
{
$endpoint = self::BASE_URL . "/crm/v3/objects/{$objectType}/search";
return $this->executeRequest(function () use ($endpoint, $payload) {
$response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$response ' . PHP_EOL . print_r($response, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$response ' . PHP_EOL . print_r($response->getResponseHeaders(), true));
return $response->toArray();
});
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$deal = $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') {
return $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
return $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
}
/**
* @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 (RateLimitException $e) {
// throw $e;
} 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);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
17945
|
NULL
|
NULL
|
NULL
|
|
17947
|
780
|
51
|
2026-05-11T10:42:37.918095+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778496157918_m1.jpg...
|
PhpStorm
|
faVsco.js – Client.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
3
68
3
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\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 Illuminate\Support\Facades\Redis;
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)
{
$cacheKey = $this->getRateLimitCacheKey();
$cachedRetryAfter = Redis::get($cacheKey);
if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {
throw new RateLimitException(
'Hubspot rate limit (cached circuit-breaker)',
(int) $cachedRetryAfter,
);
}
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'policy' => $this->parsePolicy($e),
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function getRateLimitCacheKey(): string
{
return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());
}
public function isHubspotRateLimit(Throwable $e): bool
{
if ($e instanceof BadRequest
|| $e instanceof DealApiException
|| $e instanceof ContactApiException
|| $e instanceof CompanyApiException
|| $e instanceof \GuzzleHttp\Exception\RequestException
) {
return (int) $e->getCode() === 429;
}
return false;
}
public function parseRetryAfter(Throwable $e): int
{
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$e ' . PHP_EOL . print_r($e, true));
return 5;
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;
}
}
$policy = $this->parsePolicy($e);
if ($policy === 'TEN_SECONDLY_ROLLING') {
return 10;
}
if ($policy === 'SECONDLY') {
return 1;
}
if ($policy === 'DAILY_LIMIT') {
return 600;
}
$this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [
'exception_class' => get_class($e),
]);
return 10;
}
public function parsePolicy(Throwable $e): ?string
{
if (! method_exists($e, 'getResponseBody')) {
return null;
}
$body = $e->getResponseBody();
if (is_string($body)) {
$body = json_decode($body, true) ?? [];
}
if (! is_array($body)) {
return null;
}
$policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;
return is_string($policy) ? strtoupper($policy) : null;
}
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
);
}
/**
* Execute a search request against HubSpot CRM objects with rate limiting.
*
* @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')
* @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.
* @return array The search response with 'results', 'total', 'paging' keys
* @throws RateLimitException When rate limit is hit
* @throws HubspotException On API errors
*/
public function search(string $objectType, array $payload): array
{
$endpoint = self::BASE_URL . "/crm/v3/objects/{$objectType}/search";
return $this->executeRequest(function () use ($endpoint, $payload) {
$response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$response ' . PHP_EOL . print_r($response, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$response ' . PHP_EOL . print_r($response->getResponseHeaders(), true));
return $response->toArray();
});
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$deal = $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') {
return $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
return $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
}
/**
* @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 (RateLimitException $e) {
// throw $e;
} 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);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
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":"JY-20725-handle-HS-search-rate-limit, menu","depth":5,"on_screen":true,"help_text":"Git Branch: JY-20725-handle-HS-search-rate-limit","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":"HandleHubspotRateLimitTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'HandleHubspotRateLimitTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'HandleHubspotRateLimitTest'","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":"AXStaticText","text":"3","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"68","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"3","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"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\\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 Illuminate\\Support\\Facades\\Redis;\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\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Reacts to a rate limits (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 $cacheKey = $this->getRateLimitCacheKey();\n\n $cachedRetryAfter = Redis::get($cacheKey);\n if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n (int) $cachedRetryAfter,\n );\n }\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);\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 'policy' => $this->parsePolicy($e),\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 getRateLimitCacheKey(): string\n {\n return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());\n }\n\n public function isHubspotRateLimit(Throwable $e): bool\n {\n if ($e instanceof BadRequest\n || $e instanceof DealApiException\n || $e instanceof ContactApiException\n || $e instanceof CompanyApiException\n || $e instanceof \\GuzzleHttp\\Exception\\RequestException\n ) {\n return (int) $e->getCode() === 429;\n }\n\n return false;\n }\n\n public function parseRetryAfter(Throwable $e): int\n {\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$e ' . PHP_EOL . print_r($e, true));\n return 5;\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 $policy = $this->parsePolicy($e);\n if ($policy === 'TEN_SECONDLY_ROLLING') {\n return 10;\n }\n if ($policy === 'SECONDLY') {\n return 1;\n }\n if ($policy === 'DAILY_LIMIT') {\n return 600;\n }\n\n $this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [\n 'exception_class' => get_class($e),\n ]);\n\n return 10;\n }\n\n public function parsePolicy(Throwable $e): ?string\n {\n if (! method_exists($e, 'getResponseBody')) {\n return null;\n }\n\n $body = $e->getResponseBody();\n if (is_string($body)) {\n $body = json_decode($body, true) ?? [];\n }\n\n if (! is_array($body)) {\n return null;\n }\n\n $policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;\n\n return is_string($policy) ? strtoupper($policy) : null;\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 * Execute a search request against HubSpot CRM objects with rate limiting.\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n * @return array The search response with 'results', 'total', 'paging' keys\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n */\n public function search(string $objectType, array $payload): array\n {\n $endpoint = self::BASE_URL . \"/crm/v3/objects/{$objectType}/search\";\n\n return $this->executeRequest(function () use ($endpoint, $payload) {\n $response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$response ' . PHP_EOL . print_r($response, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$response ' . PHP_EOL . print_r($response->getResponseHeaders(), true));\n\n return $response->toArray();\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 $deal = $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 return $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n return $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\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 (RateLimitException $e) {\n// throw $e;\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\\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 Illuminate\\Support\\Facades\\Redis;\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\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Reacts to a rate limits (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 $cacheKey = $this->getRateLimitCacheKey();\n\n $cachedRetryAfter = Redis::get($cacheKey);\n if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n (int) $cachedRetryAfter,\n );\n }\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);\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 'policy' => $this->parsePolicy($e),\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 getRateLimitCacheKey(): string\n {\n return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());\n }\n\n public function isHubspotRateLimit(Throwable $e): bool\n {\n if ($e instanceof BadRequest\n || $e instanceof DealApiException\n || $e instanceof ContactApiException\n || $e instanceof CompanyApiException\n || $e instanceof \\GuzzleHttp\\Exception\\RequestException\n ) {\n return (int) $e->getCode() === 429;\n }\n\n return false;\n }\n\n public function parseRetryAfter(Throwable $e): int\n {\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$e ' . PHP_EOL . print_r($e, true));\n return 5;\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 $policy = $this->parsePolicy($e);\n if ($policy === 'TEN_SECONDLY_ROLLING') {\n return 10;\n }\n if ($policy === 'SECONDLY') {\n return 1;\n }\n if ($policy === 'DAILY_LIMIT') {\n return 600;\n }\n\n $this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [\n 'exception_class' => get_class($e),\n ]);\n\n return 10;\n }\n\n public function parsePolicy(Throwable $e): ?string\n {\n if (! method_exists($e, 'getResponseBody')) {\n return null;\n }\n\n $body = $e->getResponseBody();\n if (is_string($body)) {\n $body = json_decode($body, true) ?? [];\n }\n\n if (! is_array($body)) {\n return null;\n }\n\n $policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;\n\n return is_string($policy) ? strtoupper($policy) : null;\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 * Execute a search request against HubSpot CRM objects with rate limiting.\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n * @return array The search response with 'results', 'total', 'paging' keys\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n */\n public function search(string $objectType, array $payload): array\n {\n $endpoint = self::BASE_URL . \"/crm/v3/objects/{$objectType}/search\";\n\n return $this->executeRequest(function () use ($endpoint, $payload) {\n $response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$response ' . PHP_EOL . print_r($response, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$response ' . PHP_EOL . print_r($response->getResponseHeaders(), true));\n\n return $response->toArray();\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 $deal = $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 return $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n return $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\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 (RateLimitException $e) {\n// throw $e;\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":"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":"19","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","depth":4,"on_screen":true,"value":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","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}]...
|
8673868863936031688
|
6378618611371477348
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
3
68
3
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\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 Illuminate\Support\Facades\Redis;
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)
{
$cacheKey = $this->getRateLimitCacheKey();
$cachedRetryAfter = Redis::get($cacheKey);
if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {
throw new RateLimitException(
'Hubspot rate limit (cached circuit-breaker)',
(int) $cachedRetryAfter,
);
}
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'policy' => $this->parsePolicy($e),
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function getRateLimitCacheKey(): string
{
return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());
}
public function isHubspotRateLimit(Throwable $e): bool
{
if ($e instanceof BadRequest
|| $e instanceof DealApiException
|| $e instanceof ContactApiException
|| $e instanceof CompanyApiException
|| $e instanceof \GuzzleHttp\Exception\RequestException
) {
return (int) $e->getCode() === 429;
}
return false;
}
public function parseRetryAfter(Throwable $e): int
{
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$e ' . PHP_EOL . print_r($e, true));
return 5;
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;
}
}
$policy = $this->parsePolicy($e);
if ($policy === 'TEN_SECONDLY_ROLLING') {
return 10;
}
if ($policy === 'SECONDLY') {
return 1;
}
if ($policy === 'DAILY_LIMIT') {
return 600;
}
$this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [
'exception_class' => get_class($e),
]);
return 10;
}
public function parsePolicy(Throwable $e): ?string
{
if (! method_exists($e, 'getResponseBody')) {
return null;
}
$body = $e->getResponseBody();
if (is_string($body)) {
$body = json_decode($body, true) ?? [];
}
if (! is_array($body)) {
return null;
}
$policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;
return is_string($policy) ? strtoupper($policy) : null;
}
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
);
}
/**
* Execute a search request against HubSpot CRM objects with rate limiting.
*
* @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')
* @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.
* @return array The search response with 'results', 'total', 'paging' keys
* @throws RateLimitException When rate limit is hit
* @throws HubspotException On API errors
*/
public function search(string $objectType, array $payload): array
{
$endpoint = self::BASE_URL . "/crm/v3/objects/{$objectType}/search";
return $this->executeRequest(function () use ($endpoint, $payload) {
$response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$response ' . PHP_EOL . print_r($response, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$response ' . PHP_EOL . print_r($response->getResponseHeaders(), true));
return $response->toArray();
});
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$deal = $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') {
return $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
return $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
}
/**
* @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 (RateLimitException $e) {
// throw $e;
} 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);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
17943
|
NULL
|
NULL
|
NULL
|
|
17948
|
NULL
|
0
|
2026-05-11T10:42:39.462879+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778496159462_m1.jpg...
|
PhpStorm
|
faVsco.js – Client.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
3
68
3
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\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 Illuminate\Support\Facades\Redis;
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)
{
$cacheKey = $this->getRateLimitCacheKey();
$cachedRetryAfter = Redis::get($cacheKey);
if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {
throw new RateLimitException(
'Hubspot rate limit (cached circuit-breaker)',
(int) $cachedRetryAfter,
);
}
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'policy' => $this->parsePolicy($e),
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function getRateLimitCacheKey(): string
{
return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());
}
public function isHubspotRateLimit(Throwable $e): bool
{
if ($e instanceof BadRequest
|| $e instanceof DealApiException
|| $e instanceof ContactApiException
|| $e instanceof CompanyApiException
|| $e instanceof \GuzzleHttp\Exception\RequestException
) {
return (int) $e->getCode() === 429;
}
return false;
}
public function parseRetryAfter(Throwable $e): int
{
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$e ' . PHP_EOL . print_r($e, true));
return 5;
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;
}
}
$policy = $this->parsePolicy($e);
if ($policy === 'TEN_SECONDLY_ROLLING') {
return 10;
}
if ($policy === 'SECONDLY') {
return 1;
}
if ($policy === 'DAILY_LIMIT') {
return 600;
}
$this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [
'exception_class' => get_class($e),
]);
return 10;
}
public function parsePolicy(Throwable $e): ?string
{
if (! method_exists($e, 'getResponseBody')) {
return null;
}
$body = $e->getResponseBody();
if (is_string($body)) {
$body = json_decode($body, true) ?? [];
}
if (! is_array($body)) {
return null;
}
$policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;
return is_string($policy) ? strtoupper($policy) : null;
}
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
);
}
/**
* Execute a search request against HubSpot CRM objects with rate limiting.
*
* @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')
* @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.
* @return array The search response with 'results', 'total', 'paging' keys
* @throws RateLimitException When rate limit is hit
* @throws HubspotException On API errors
*/
public function search(string $objectType, array $payload): array
{
$endpoint = self::BASE_URL . "/crm/v3/objects/{$objectType}/search";
return $this->executeRequest(function () use ($endpoint, $payload) {
$response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$response ' . PHP_EOL . print_r($response, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$response ' . PHP_EOL . print_r($response->getResponseHeaders(), true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$response ' . PHP_EOL . print_r($response->getResponseHeaders(), true));
return $response->toArray();
});
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$deal = $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') {
return $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
return $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
}
/**
* @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 (RateLimitException $e) {
// throw $e;
} 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);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
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":"JY-20725-handle-HS-search-rate-limit, menu","depth":5,"on_screen":true,"help_text":"Git Branch: JY-20725-handle-HS-search-rate-limit","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":"HandleHubspotRateLimitTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'HandleHubspotRateLimitTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'HandleHubspotRateLimitTest'","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":"AXStaticText","text":"3","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"68","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"3","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"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\\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 Illuminate\\Support\\Facades\\Redis;\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\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Reacts to a rate limits (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 $cacheKey = $this->getRateLimitCacheKey();\n\n $cachedRetryAfter = Redis::get($cacheKey);\n if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n (int) $cachedRetryAfter,\n );\n }\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);\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 'policy' => $this->parsePolicy($e),\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 getRateLimitCacheKey(): string\n {\n return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());\n }\n\n public function isHubspotRateLimit(Throwable $e): bool\n {\n if ($e instanceof BadRequest\n || $e instanceof DealApiException\n || $e instanceof ContactApiException\n || $e instanceof CompanyApiException\n || $e instanceof \\GuzzleHttp\\Exception\\RequestException\n ) {\n return (int) $e->getCode() === 429;\n }\n\n return false;\n }\n\n public function parseRetryAfter(Throwable $e): int\n {\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$e ' . PHP_EOL . print_r($e, true));\n return 5;\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 $policy = $this->parsePolicy($e);\n if ($policy === 'TEN_SECONDLY_ROLLING') {\n return 10;\n }\n if ($policy === 'SECONDLY') {\n return 1;\n }\n if ($policy === 'DAILY_LIMIT') {\n return 600;\n }\n\n $this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [\n 'exception_class' => get_class($e),\n ]);\n\n return 10;\n }\n\n public function parsePolicy(Throwable $e): ?string\n {\n if (! method_exists($e, 'getResponseBody')) {\n return null;\n }\n\n $body = $e->getResponseBody();\n if (is_string($body)) {\n $body = json_decode($body, true) ?? [];\n }\n\n if (! is_array($body)) {\n return null;\n }\n\n $policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;\n\n return is_string($policy) ? strtoupper($policy) : null;\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 * Execute a search request against HubSpot CRM objects with rate limiting.\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n * @return array The search response with 'results', 'total', 'paging' keys\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n */\n public function search(string $objectType, array $payload): array\n {\n $endpoint = self::BASE_URL . \"/crm/v3/objects/{$objectType}/search\";\n\n return $this->executeRequest(function () use ($endpoint, $payload) {\n $response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$response ' . PHP_EOL . print_r($response, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$response ' . PHP_EOL . print_r($response->getResponseHeaders(), true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$response ' . PHP_EOL . print_r($response->getResponseHeaders(), true));\n\n return $response->toArray();\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 $deal = $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 return $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n return $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\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 (RateLimitException $e) {\n// throw $e;\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\\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 Illuminate\\Support\\Facades\\Redis;\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\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Reacts to a rate limits (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 $cacheKey = $this->getRateLimitCacheKey();\n\n $cachedRetryAfter = Redis::get($cacheKey);\n if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n (int) $cachedRetryAfter,\n );\n }\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);\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 'policy' => $this->parsePolicy($e),\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 getRateLimitCacheKey(): string\n {\n return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());\n }\n\n public function isHubspotRateLimit(Throwable $e): bool\n {\n if ($e instanceof BadRequest\n || $e instanceof DealApiException\n || $e instanceof ContactApiException\n || $e instanceof CompanyApiException\n || $e instanceof \\GuzzleHttp\\Exception\\RequestException\n ) {\n return (int) $e->getCode() === 429;\n }\n\n return false;\n }\n\n public function parseRetryAfter(Throwable $e): int\n {\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$e ' . PHP_EOL . print_r($e, true));\n return 5;\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 $policy = $this->parsePolicy($e);\n if ($policy === 'TEN_SECONDLY_ROLLING') {\n return 10;\n }\n if ($policy === 'SECONDLY') {\n return 1;\n }\n if ($policy === 'DAILY_LIMIT') {\n return 600;\n }\n\n $this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [\n 'exception_class' => get_class($e),\n ]);\n\n return 10;\n }\n\n public function parsePolicy(Throwable $e): ?string\n {\n if (! method_exists($e, 'getResponseBody')) {\n return null;\n }\n\n $body = $e->getResponseBody();\n if (is_string($body)) {\n $body = json_decode($body, true) ?? [];\n }\n\n if (! is_array($body)) {\n return null;\n }\n\n $policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;\n\n return is_string($policy) ? strtoupper($policy) : null;\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 * Execute a search request against HubSpot CRM objects with rate limiting.\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n * @return array The search response with 'results', 'total', 'paging' keys\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n */\n public function search(string $objectType, array $payload): array\n {\n $endpoint = self::BASE_URL . \"/crm/v3/objects/{$objectType}/search\";\n\n return $this->executeRequest(function () use ($endpoint, $payload) {\n $response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$response ' . PHP_EOL . print_r($response, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$response ' . PHP_EOL . print_r($response->getResponseHeaders(), true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$response ' . PHP_EOL . print_r($response->getResponseHeaders(), true));\n\n return $response->toArray();\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 $deal = $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 return $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n return $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\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 (RateLimitException $e) {\n// throw $e;\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":"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":"19","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","depth":4,"on_screen":true,"value":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","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}]...
|
1947971519123448970
|
6378618611371477348
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
3
68
3
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\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 Illuminate\Support\Facades\Redis;
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)
{
$cacheKey = $this->getRateLimitCacheKey();
$cachedRetryAfter = Redis::get($cacheKey);
if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {
throw new RateLimitException(
'Hubspot rate limit (cached circuit-breaker)',
(int) $cachedRetryAfter,
);
}
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'policy' => $this->parsePolicy($e),
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function getRateLimitCacheKey(): string
{
return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());
}
public function isHubspotRateLimit(Throwable $e): bool
{
if ($e instanceof BadRequest
|| $e instanceof DealApiException
|| $e instanceof ContactApiException
|| $e instanceof CompanyApiException
|| $e instanceof \GuzzleHttp\Exception\RequestException
) {
return (int) $e->getCode() === 429;
}
return false;
}
public function parseRetryAfter(Throwable $e): int
{
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$e ' . PHP_EOL . print_r($e, true));
return 5;
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;
}
}
$policy = $this->parsePolicy($e);
if ($policy === 'TEN_SECONDLY_ROLLING') {
return 10;
}
if ($policy === 'SECONDLY') {
return 1;
}
if ($policy === 'DAILY_LIMIT') {
return 600;
}
$this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [
'exception_class' => get_class($e),
]);
return 10;
}
public function parsePolicy(Throwable $e): ?string
{
if (! method_exists($e, 'getResponseBody')) {
return null;
}
$body = $e->getResponseBody();
if (is_string($body)) {
$body = json_decode($body, true) ?? [];
}
if (! is_array($body)) {
return null;
}
$policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;
return is_string($policy) ? strtoupper($policy) : null;
}
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
);
}
/**
* Execute a search request against HubSpot CRM objects with rate limiting.
*
* @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')
* @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.
* @return array The search response with 'results', 'total', 'paging' keys
* @throws RateLimitException When rate limit is hit
* @throws HubspotException On API errors
*/
public function search(string $objectType, array $payload): array
{
$endpoint = self::BASE_URL . "/crm/v3/objects/{$objectType}/search";
return $this->executeRequest(function () use ($endpoint, $payload) {
$response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$response ' . PHP_EOL . print_r($response, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$response ' . PHP_EOL . print_r($response->getResponseHeaders(), true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$response ' . PHP_EOL . print_r($response->getResponseHeaders(), true));
return $response->toArray();
});
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$deal = $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') {
return $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
return $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
}
/**
* @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 (RateLimitException $e) {
// throw $e;
} 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);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
17949
|
NULL
|
0
|
2026-05-11T10:42:39.462895+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778496159462_m2.jpg...
|
PhpStorm
|
faVsco.js – Client.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
3
68
3
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\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 Illuminate\Support\Facades\Redis;
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)
{
$cacheKey = $this->getRateLimitCacheKey();
$cachedRetryAfter = Redis::get($cacheKey);
if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {
throw new RateLimitException(
'Hubspot rate limit (cached circuit-breaker)',
(int) $cachedRetryAfter,
);
}
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'policy' => $this->parsePolicy($e),
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function getRateLimitCacheKey(): string
{
return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());
}
public function isHubspotRateLimit(Throwable $e): bool
{
if ($e instanceof BadRequest
|| $e instanceof DealApiException
|| $e instanceof ContactApiException
|| $e instanceof CompanyApiException
|| $e instanceof \GuzzleHttp\Exception\RequestException
) {
return (int) $e->getCode() === 429;
}
return false;
}
public function parseRetryAfter(Throwable $e): int
{
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$e ' . PHP_EOL . print_r($e, true));
return 5;
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;
}
}
$policy = $this->parsePolicy($e);
if ($policy === 'TEN_SECONDLY_ROLLING') {
return 10;
}
if ($policy === 'SECONDLY') {
return 1;
}
if ($policy === 'DAILY_LIMIT') {
return 600;
}
$this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [
'exception_class' => get_class($e),
]);
return 10;
}
public function parsePolicy(Throwable $e): ?string
{
if (! method_exists($e, 'getResponseBody')) {
return null;
}
$body = $e->getResponseBody();
if (is_string($body)) {
$body = json_decode($body, true) ?? [];
}
if (! is_array($body)) {
return null;
}
$policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;
return is_string($policy) ? strtoupper($policy) : null;
}
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
);
}
/**
* Execute a search request against HubSpot CRM objects with rate limiting.
*
* @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')
* @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.
* @return array The search response with 'results', 'total', 'paging' keys
* @throws RateLimitException When rate limit is hit
* @throws HubspotException On API errors
*/
public function search(string $objectType, array $payload): array
{
$endpoint = self::BASE_URL . "/crm/v3/objects/{$objectType}/search";
return $this->executeRequest(function () use ($endpoint, $payload) {
$response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$response ' . PHP_EOL . print_r($response, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$response ' . PHP_EOL . print_r($response->getResponseHeaders(), true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$response ' . PHP_EOL . print_r($response->getResponseHeaders(), true));
return $response->toArray();
});
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$deal = $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') {
return $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
return $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
}
/**
* @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 (RateLimitException $e) {
// throw $e;
} 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);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
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":"JY-20725-handle-HS-search-rate-limit, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.09541223,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: JY-20725-handle-HS-search-rate-limit","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.82413566,"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":"HandleHubspotRateLimitTest","depth":6,"bounds":{"left":0.8394282,"top":0.019952115,"width":0.076130316,"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 'HandleHubspotRateLimitTest'","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 'HandleHubspotRateLimitTest'","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":"AXStaticText","text":"3","depth":4,"bounds":{"left":0.5515292,"top":0.15003991,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"68","depth":4,"bounds":{"left":0.56150264,"top":0.15003991,"width":0.010305851,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"3","depth":4,"bounds":{"left":0.5738032,"top":0.15003991,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.5834442,"top":0.14844373,"width":0.00731383,"height":0.018355945},"on_screen":true,"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.59075797,"top":0.14844373,"width":0.006981383,"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\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\\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 Illuminate\\Support\\Facades\\Redis;\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\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Reacts to a rate limits (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 $cacheKey = $this->getRateLimitCacheKey();\n\n $cachedRetryAfter = Redis::get($cacheKey);\n if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n (int) $cachedRetryAfter,\n );\n }\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);\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 'policy' => $this->parsePolicy($e),\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 getRateLimitCacheKey(): string\n {\n return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());\n }\n\n public function isHubspotRateLimit(Throwable $e): bool\n {\n if ($e instanceof BadRequest\n || $e instanceof DealApiException\n || $e instanceof ContactApiException\n || $e instanceof CompanyApiException\n || $e instanceof \\GuzzleHttp\\Exception\\RequestException\n ) {\n return (int) $e->getCode() === 429;\n }\n\n return false;\n }\n\n public function parseRetryAfter(Throwable $e): int\n {\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$e ' . PHP_EOL . print_r($e, true));\n return 5;\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 $policy = $this->parsePolicy($e);\n if ($policy === 'TEN_SECONDLY_ROLLING') {\n return 10;\n }\n if ($policy === 'SECONDLY') {\n return 1;\n }\n if ($policy === 'DAILY_LIMIT') {\n return 600;\n }\n\n $this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [\n 'exception_class' => get_class($e),\n ]);\n\n return 10;\n }\n\n public function parsePolicy(Throwable $e): ?string\n {\n if (! method_exists($e, 'getResponseBody')) {\n return null;\n }\n\n $body = $e->getResponseBody();\n if (is_string($body)) {\n $body = json_decode($body, true) ?? [];\n }\n\n if (! is_array($body)) {\n return null;\n }\n\n $policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;\n\n return is_string($policy) ? strtoupper($policy) : null;\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 * Execute a search request against HubSpot CRM objects with rate limiting.\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n * @return array The search response with 'results', 'total', 'paging' keys\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n */\n public function search(string $objectType, array $payload): array\n {\n $endpoint = self::BASE_URL . \"/crm/v3/objects/{$objectType}/search\";\n\n return $this->executeRequest(function () use ($endpoint, $payload) {\n $response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$response ' . PHP_EOL . print_r($response, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$response ' . PHP_EOL . print_r($response->getResponseHeaders(), true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$response ' . PHP_EOL . print_r($response->getResponseHeaders(), true));\n\n return $response->toArray();\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 $deal = $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 return $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n return $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\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 (RateLimitException $e) {\n// throw $e;\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\\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 Illuminate\\Support\\Facades\\Redis;\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\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Reacts to a rate limits (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 $cacheKey = $this->getRateLimitCacheKey();\n\n $cachedRetryAfter = Redis::get($cacheKey);\n if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n (int) $cachedRetryAfter,\n );\n }\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);\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 'policy' => $this->parsePolicy($e),\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 getRateLimitCacheKey(): string\n {\n return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());\n }\n\n public function isHubspotRateLimit(Throwable $e): bool\n {\n if ($e instanceof BadRequest\n || $e instanceof DealApiException\n || $e instanceof ContactApiException\n || $e instanceof CompanyApiException\n || $e instanceof \\GuzzleHttp\\Exception\\RequestException\n ) {\n return (int) $e->getCode() === 429;\n }\n\n return false;\n }\n\n public function parseRetryAfter(Throwable $e): int\n {\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$e ' . PHP_EOL . print_r($e, true));\n return 5;\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 $policy = $this->parsePolicy($e);\n if ($policy === 'TEN_SECONDLY_ROLLING') {\n return 10;\n }\n if ($policy === 'SECONDLY') {\n return 1;\n }\n if ($policy === 'DAILY_LIMIT') {\n return 600;\n }\n\n $this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [\n 'exception_class' => get_class($e),\n ]);\n\n return 10;\n }\n\n public function parsePolicy(Throwable $e): ?string\n {\n if (! method_exists($e, 'getResponseBody')) {\n return null;\n }\n\n $body = $e->getResponseBody();\n if (is_string($body)) {\n $body = json_decode($body, true) ?? [];\n }\n\n if (! is_array($body)) {\n return null;\n }\n\n $policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;\n\n return is_string($policy) ? strtoupper($policy) : null;\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 * Execute a search request against HubSpot CRM objects with rate limiting.\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n * @return array The search response with 'results', 'total', 'paging' keys\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n */\n public function search(string $objectType, array $payload): array\n {\n $endpoint = self::BASE_URL . \"/crm/v3/objects/{$objectType}/search\";\n\n return $this->executeRequest(function () use ($endpoint, $payload) {\n $response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$response ' . PHP_EOL . print_r($response, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$response ' . PHP_EOL . print_r($response->getResponseHeaders(), true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$response ' . PHP_EOL . print_r($response->getResponseHeaders(), true));\n\n return $response->toArray();\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 $deal = $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 return $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n return $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\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 (RateLimitException $e) {\n// throw $e;\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":"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":"19","depth":4,"bounds":{"left":0.96276593,"top":0.07581804,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.9740692,"top":0.074221864,"width":0.00731383,"height":0.018355945},"on_screen":true,"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.98138297,"top":0.074221864,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","depth":4,"bounds":{"left":0.61702126,"top":0.0726257,"width":0.38297874,"height":0.9066241},"on_screen":true,"lines":[{"char_start":273,"char_count":32,"bounds":{"left":0.61702126,"top":0.0,"width":0.080119684,"height":0.014365523}},{"char_start":305,"char_count":79,"bounds":{"left":0.61702126,"top":0.0,"width":0.20212767,"height":0.014365523}},{"char_start":384,"char_count":18,"bounds":{"left":0.61702126,"top":0.0,"width":0.043882977,"height":0.014365523}},{"char_start":402,"char_count":21,"bounds":{"left":0.61702126,"top":0.0,"width":0.051861703,"height":0.014365523}},{"char_start":423,"char_count":48,"bounds":{"left":0.61702126,"top":0.0,"width":0.12167553,"height":0.014365523}},{"char_start":471,"char_count":72,"bounds":{"left":0.61702126,"top":0.0015961692,"width":0.18384309,"height":0.014365523}},{"char_start":543,"char_count":40,"bounds":{"left":0.61702126,"top":0.01915403,"width":0.10106383,"height":0.014365523}},{"char_start":583,"char_count":41,"bounds":{"left":0.61702126,"top":0.03671189,"width":0.10372341,"height":0.014365523}},{"char_start":624,"char_count":72,"bounds":{"left":0.61702126,"top":0.054269753,"width":0.18384309,"height":0.014365523}},{"char_start":696,"char_count":219,"bounds":{"left":0.61702126,"top":0.07182761,"width":0.38297874,"height":0.014365523}},{"char_start":915,"char_count":83,"bounds":{"left":0.61702126,"top":0.08938547,"width":0.21243352,"height":0.014365523}},{"char_start":998,"char_count":20,"bounds":{"left":0.61702126,"top":0.10694334,"width":0.04920213,"height":0.014365523}},{"char_start":1018,"char_count":17,"bounds":{"left":0.61702126,"top":0.1245012,"width":0.041223403,"height":0.014365523}},{"char_start":1035,"char_count":203,"bounds":{"left":0.61702126,"top":0.14205906,"width":0.38297874,"height":0.014365523}},{"char_start":1238,"char_count":22,"bounds":{"left":0.61702126,"top":0.15961692,"width":0.05418883,"height":0.014365523}},{"char_start":1260,"char_count":23,"bounds":{"left":0.61702126,"top":0.17717478,"width":0.056848403,"height":0.014365523}},{"char_start":1283,"char_count":10,"bounds":{"left":0.61702126,"top":0.19473264,"width":0.023271276,"height":0.014365523}},{"char_start":1293,"char_count":27,"bounds":{"left":0.61702126,"top":0.2122905,"width":0.06715426,"height":0.014365523}},{"char_start":1320,"char_count":26,"bounds":{"left":0.61702126,"top":0.22984837,"width":0.06482713,"height":0.014365523}},{"char_start":1346,"char_count":23,"bounds":{"left":0.61702126,"top":0.24740623,"width":0.056848403,"height":0.014365523}},{"char_start":1369,"char_count":28,"bounds":{"left":0.61702126,"top":0.26496407,"width":0.06981383,"height":0.014365523}},{"char_start":1397,"char_count":57,"bounds":{"left":0.61702126,"top":0.28252193,"width":0.14494681,"height":0.014365523}}],"value":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","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}]...
|
1947971519123448970
|
6378618611371477348
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
3
68
3
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\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 Illuminate\Support\Facades\Redis;
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)
{
$cacheKey = $this->getRateLimitCacheKey();
$cachedRetryAfter = Redis::get($cacheKey);
if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {
throw new RateLimitException(
'Hubspot rate limit (cached circuit-breaker)',
(int) $cachedRetryAfter,
);
}
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'policy' => $this->parsePolicy($e),
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function getRateLimitCacheKey(): string
{
return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());
}
public function isHubspotRateLimit(Throwable $e): bool
{
if ($e instanceof BadRequest
|| $e instanceof DealApiException
|| $e instanceof ContactApiException
|| $e instanceof CompanyApiException
|| $e instanceof \GuzzleHttp\Exception\RequestException
) {
return (int) $e->getCode() === 429;
}
return false;
}
public function parseRetryAfter(Throwable $e): int
{
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$e ' . PHP_EOL . print_r($e, true));
return 5;
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;
}
}
$policy = $this->parsePolicy($e);
if ($policy === 'TEN_SECONDLY_ROLLING') {
return 10;
}
if ($policy === 'SECONDLY') {
return 1;
}
if ($policy === 'DAILY_LIMIT') {
return 600;
}
$this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [
'exception_class' => get_class($e),
]);
return 10;
}
public function parsePolicy(Throwable $e): ?string
{
if (! method_exists($e, 'getResponseBody')) {
return null;
}
$body = $e->getResponseBody();
if (is_string($body)) {
$body = json_decode($body, true) ?? [];
}
if (! is_array($body)) {
return null;
}
$policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;
return is_string($policy) ? strtoupper($policy) : null;
}
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
);
}
/**
* Execute a search request against HubSpot CRM objects with rate limiting.
*
* @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')
* @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.
* @return array The search response with 'results', 'total', 'paging' keys
* @throws RateLimitException When rate limit is hit
* @throws HubspotException On API errors
*/
public function search(string $objectType, array $payload): array
{
$endpoint = self::BASE_URL . "/crm/v3/objects/{$objectType}/search";
return $this->executeRequest(function () use ($endpoint, $payload) {
$response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$response ' . PHP_EOL . print_r($response, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$response ' . PHP_EOL . print_r($response->getResponseHeaders(), true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$response ' . PHP_EOL . print_r($response->getResponseHeaders(), true));
return $response->toArray();
});
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$deal = $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') {
return $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
return $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
}
/**
* @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 (RateLimitException $e) {
// throw $e;
} 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);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
17950
|
782
|
0
|
2026-05-11T10:42:48.128824+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778496168128_m1.jpg...
|
PhpStorm
|
faVsco.js – Client.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
4
71
3
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\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 Illuminate\Support\Facades\Redis;
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)
{
$cacheKey = $this->getRateLimitCacheKey();
$cachedRetryAfter = Redis::get($cacheKey);
if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {
throw new RateLimitException(
'Hubspot rate limit (cached circuit-breaker)',
(int) $cachedRetryAfter,
);
}
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'policy' => $this->parsePolicy($e),
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function getRateLimitCacheKey(): string
{
return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());
}
public function isHubspotRateLimit(Throwable $e): bool
{
if ($e instanceof BadRequest
|| $e instanceof DealApiException
|| $e instanceof ContactApiException
|| $e instanceof CompanyApiException
|| $e instanceof \GuzzleHttp\Exception\RequestException
) {
return (int) $e->getCode() === 429;
}
return false;
}
public function parseRetryAfter(Throwable $e): int
{
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$e ' . PHP_EOL . print_r($e, true));
return 5;
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;
}
}
$policy = $this->parsePolicy($e);
if ($policy === 'TEN_SECONDLY_ROLLING') {
return 10;
}
if ($policy === 'SECONDLY') {
return 1;
}
if ($policy === 'DAILY_LIMIT') {
return 600;
}
$this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [
'exception_class' => get_class($e),
]);
return 10;
}
public function parsePolicy(Throwable $e): ?string
{
if (! method_exists($e, 'getResponseBody')) {
return null;
}
$body = $e->getResponseBody();
if (is_string($body)) {
$body = json_decode($body, true) ?? [];
}
if (! is_array($body)) {
return null;
}
$policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;
return is_string($policy) ? strtoupper($policy) : null;
}
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
);
}
/**
* Execute a search request against HubSpot CRM objects with rate limiting.
*
* @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')
* @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.
* @return array The search response with 'results', 'total', 'paging' keys
* @throws RateLimitException When rate limit is hit
* @throws HubspotException On API errors
*/
public function search(string $objectType, array $payload): array
{
$endpoint = self::BASE_URL . "/crm/v3/objects/{$objectType}/search";
return $this->executeRequest(function () use ($endpoint, $payload) {
$response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$response ' . PHP_EOL . print_r($response, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$response ' . PHP_EOL . print_r($response->getResponseHeaders(), true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$response ' . PHP_EOL . print_r($response->getHeaders(), true));
return $response->toArray();
});
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$deal = $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') {
return $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
return $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
}
/**
* @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 (RateLimitException $e) {
// throw $e;
} 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);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
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":"JY-20725-handle-HS-search-rate-limit, menu","depth":5,"on_screen":true,"help_text":"Git Branch: JY-20725-handle-HS-search-rate-limit","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":"HandleHubspotRateLimitTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'HandleHubspotRateLimitTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'HandleHubspotRateLimitTest'","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":"AXStaticText","text":"4","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"71","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"3","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"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\\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 Illuminate\\Support\\Facades\\Redis;\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\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Reacts to a rate limits (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 $cacheKey = $this->getRateLimitCacheKey();\n\n $cachedRetryAfter = Redis::get($cacheKey);\n if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n (int) $cachedRetryAfter,\n );\n }\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);\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 'policy' => $this->parsePolicy($e),\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 getRateLimitCacheKey(): string\n {\n return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());\n }\n\n public function isHubspotRateLimit(Throwable $e): bool\n {\n if ($e instanceof BadRequest\n || $e instanceof DealApiException\n || $e instanceof ContactApiException\n || $e instanceof CompanyApiException\n || $e instanceof \\GuzzleHttp\\Exception\\RequestException\n ) {\n return (int) $e->getCode() === 429;\n }\n\n return false;\n }\n\n public function parseRetryAfter(Throwable $e): int\n {\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$e ' . PHP_EOL . print_r($e, true));\n return 5;\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 $policy = $this->parsePolicy($e);\n if ($policy === 'TEN_SECONDLY_ROLLING') {\n return 10;\n }\n if ($policy === 'SECONDLY') {\n return 1;\n }\n if ($policy === 'DAILY_LIMIT') {\n return 600;\n }\n\n $this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [\n 'exception_class' => get_class($e),\n ]);\n\n return 10;\n }\n\n public function parsePolicy(Throwable $e): ?string\n {\n if (! method_exists($e, 'getResponseBody')) {\n return null;\n }\n\n $body = $e->getResponseBody();\n if (is_string($body)) {\n $body = json_decode($body, true) ?? [];\n }\n\n if (! is_array($body)) {\n return null;\n }\n\n $policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;\n\n return is_string($policy) ? strtoupper($policy) : null;\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 * Execute a search request against HubSpot CRM objects with rate limiting.\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n * @return array The search response with 'results', 'total', 'paging' keys\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n */\n public function search(string $objectType, array $payload): array\n {\n $endpoint = self::BASE_URL . \"/crm/v3/objects/{$objectType}/search\";\n\n return $this->executeRequest(function () use ($endpoint, $payload) {\n $response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$response ' . PHP_EOL . print_r($response, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$response ' . PHP_EOL . print_r($response->getResponseHeaders(), true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$response ' . PHP_EOL . print_r($response->getHeaders(), true));\n\n return $response->toArray();\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 $deal = $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 return $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n return $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\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 (RateLimitException $e) {\n// throw $e;\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\\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 Illuminate\\Support\\Facades\\Redis;\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\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Reacts to a rate limits (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 $cacheKey = $this->getRateLimitCacheKey();\n\n $cachedRetryAfter = Redis::get($cacheKey);\n if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n (int) $cachedRetryAfter,\n );\n }\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);\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 'policy' => $this->parsePolicy($e),\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 getRateLimitCacheKey(): string\n {\n return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());\n }\n\n public function isHubspotRateLimit(Throwable $e): bool\n {\n if ($e instanceof BadRequest\n || $e instanceof DealApiException\n || $e instanceof ContactApiException\n || $e instanceof CompanyApiException\n || $e instanceof \\GuzzleHttp\\Exception\\RequestException\n ) {\n return (int) $e->getCode() === 429;\n }\n\n return false;\n }\n\n public function parseRetryAfter(Throwable $e): int\n {\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$e ' . PHP_EOL . print_r($e, true));\n return 5;\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 $policy = $this->parsePolicy($e);\n if ($policy === 'TEN_SECONDLY_ROLLING') {\n return 10;\n }\n if ($policy === 'SECONDLY') {\n return 1;\n }\n if ($policy === 'DAILY_LIMIT') {\n return 600;\n }\n\n $this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [\n 'exception_class' => get_class($e),\n ]);\n\n return 10;\n }\n\n public function parsePolicy(Throwable $e): ?string\n {\n if (! method_exists($e, 'getResponseBody')) {\n return null;\n }\n\n $body = $e->getResponseBody();\n if (is_string($body)) {\n $body = json_decode($body, true) ?? [];\n }\n\n if (! is_array($body)) {\n return null;\n }\n\n $policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;\n\n return is_string($policy) ? strtoupper($policy) : null;\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 * Execute a search request against HubSpot CRM objects with rate limiting.\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n * @return array The search response with 'results', 'total', 'paging' keys\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n */\n public function search(string $objectType, array $payload): array\n {\n $endpoint = self::BASE_URL . \"/crm/v3/objects/{$objectType}/search\";\n\n return $this->executeRequest(function () use ($endpoint, $payload) {\n $response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$response ' . PHP_EOL . print_r($response, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$response ' . PHP_EOL . print_r($response->getResponseHeaders(), true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$response ' . PHP_EOL . print_r($response->getHeaders(), true));\n\n return $response->toArray();\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 $deal = $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 return $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n return $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\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 (RateLimitException $e) {\n// throw $e;\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":"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":"19","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","depth":4,"on_screen":true,"value":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","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}]...
|
4531694917553906031
|
6378618611371477348
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
4
71
3
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\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 Illuminate\Support\Facades\Redis;
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)
{
$cacheKey = $this->getRateLimitCacheKey();
$cachedRetryAfter = Redis::get($cacheKey);
if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {
throw new RateLimitException(
'Hubspot rate limit (cached circuit-breaker)',
(int) $cachedRetryAfter,
);
}
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'policy' => $this->parsePolicy($e),
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function getRateLimitCacheKey(): string
{
return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());
}
public function isHubspotRateLimit(Throwable $e): bool
{
if ($e instanceof BadRequest
|| $e instanceof DealApiException
|| $e instanceof ContactApiException
|| $e instanceof CompanyApiException
|| $e instanceof \GuzzleHttp\Exception\RequestException
) {
return (int) $e->getCode() === 429;
}
return false;
}
public function parseRetryAfter(Throwable $e): int
{
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$e ' . PHP_EOL . print_r($e, true));
return 5;
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;
}
}
$policy = $this->parsePolicy($e);
if ($policy === 'TEN_SECONDLY_ROLLING') {
return 10;
}
if ($policy === 'SECONDLY') {
return 1;
}
if ($policy === 'DAILY_LIMIT') {
return 600;
}
$this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [
'exception_class' => get_class($e),
]);
return 10;
}
public function parsePolicy(Throwable $e): ?string
{
if (! method_exists($e, 'getResponseBody')) {
return null;
}
$body = $e->getResponseBody();
if (is_string($body)) {
$body = json_decode($body, true) ?? [];
}
if (! is_array($body)) {
return null;
}
$policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;
return is_string($policy) ? strtoupper($policy) : null;
}
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
);
}
/**
* Execute a search request against HubSpot CRM objects with rate limiting.
*
* @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')
* @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.
* @return array The search response with 'results', 'total', 'paging' keys
* @throws RateLimitException When rate limit is hit
* @throws HubspotException On API errors
*/
public function search(string $objectType, array $payload): array
{
$endpoint = self::BASE_URL . "/crm/v3/objects/{$objectType}/search";
return $this->executeRequest(function () use ($endpoint, $payload) {
$response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$response ' . PHP_EOL . print_r($response, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$response ' . PHP_EOL . print_r($response->getResponseHeaders(), true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$response ' . PHP_EOL . print_r($response->getHeaders(), true));
return $response->toArray();
});
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$deal = $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') {
return $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
return $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
}
/**
* @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 (RateLimitException $e) {
// throw $e;
} 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);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
17948
|
NULL
|
NULL
|
NULL
|
|
17951
|
783
|
0
|
2026-05-11T10:42:48.138861+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778496168138_m2.jpg...
|
PhpStorm
|
faVsco.js – Client.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
4
71
3
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\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 Illuminate\Support\Facades\Redis;
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)
{
$cacheKey = $this->getRateLimitCacheKey();
$cachedRetryAfter = Redis::get($cacheKey);
if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {
throw new RateLimitException(
'Hubspot rate limit (cached circuit-breaker)',
(int) $cachedRetryAfter,
);
}
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'policy' => $this->parsePolicy($e),
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function getRateLimitCacheKey(): string
{
return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());
}
public function isHubspotRateLimit(Throwable $e): bool
{
if ($e instanceof BadRequest
|| $e instanceof DealApiException
|| $e instanceof ContactApiException
|| $e instanceof CompanyApiException
|| $e instanceof \GuzzleHttp\Exception\RequestException
) {
return (int) $e->getCode() === 429;
}
return false;
}
public function parseRetryAfter(Throwable $e): int
{
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$e ' . PHP_EOL . print_r($e, true));
return 5;
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;
}
}
$policy = $this->parsePolicy($e);
if ($policy === 'TEN_SECONDLY_ROLLING') {
return 10;
}
if ($policy === 'SECONDLY') {
return 1;
}
if ($policy === 'DAILY_LIMIT') {
return 600;
}
$this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [
'exception_class' => get_class($e),
]);
return 10;
}
public function parsePolicy(Throwable $e): ?string
{
if (! method_exists($e, 'getResponseBody')) {
return null;
}
$body = $e->getResponseBody();
if (is_string($body)) {
$body = json_decode($body, true) ?? [];
}
if (! is_array($body)) {
return null;
}
$policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;
return is_string($policy) ? strtoupper($policy) : null;
}
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
);
}
/**
* Execute a search request against HubSpot CRM objects with rate limiting.
*
* @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')
* @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.
* @return array The search response with 'results', 'total', 'paging' keys
* @throws RateLimitException When rate limit is hit
* @throws HubspotException On API errors
*/
public function search(string $objectType, array $payload): array
{
$endpoint = self::BASE_URL . "/crm/v3/objects/{$objectType}/search";
return $this->executeRequest(function () use ($endpoint, $payload) {
$response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$response ' . PHP_EOL . print_r($response, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$response ' . PHP_EOL . print_r($response->getResponseHeaders(), true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$response ' . PHP_EOL . print_r($response->getHeaders(), true));
return $response->toArray();
});
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$deal = $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') {
return $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
return $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
}
/**
* @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 (RateLimitException $e) {
// throw $e;
} 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);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
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":"JY-20725-handle-HS-search-rate-limit, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.09541223,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: JY-20725-handle-HS-search-rate-limit","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.82413566,"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":"HandleHubspotRateLimitTest","depth":6,"bounds":{"left":0.8394282,"top":0.019952115,"width":0.076130316,"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 'HandleHubspotRateLimitTest'","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 'HandleHubspotRateLimitTest'","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":"AXStaticText","text":"4","depth":4,"bounds":{"left":0.5525266,"top":0.15003991,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"71","depth":4,"bounds":{"left":0.5625,"top":0.15003991,"width":0.00930851,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"3","depth":4,"bounds":{"left":0.5738032,"top":0.15003991,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.5834442,"top":0.14844373,"width":0.00731383,"height":0.018355945},"on_screen":true,"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.59075797,"top":0.14844373,"width":0.006981383,"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\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\\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 Illuminate\\Support\\Facades\\Redis;\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\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Reacts to a rate limits (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 $cacheKey = $this->getRateLimitCacheKey();\n\n $cachedRetryAfter = Redis::get($cacheKey);\n if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n (int) $cachedRetryAfter,\n );\n }\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);\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 'policy' => $this->parsePolicy($e),\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 getRateLimitCacheKey(): string\n {\n return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());\n }\n\n public function isHubspotRateLimit(Throwable $e): bool\n {\n if ($e instanceof BadRequest\n || $e instanceof DealApiException\n || $e instanceof ContactApiException\n || $e instanceof CompanyApiException\n || $e instanceof \\GuzzleHttp\\Exception\\RequestException\n ) {\n return (int) $e->getCode() === 429;\n }\n\n return false;\n }\n\n public function parseRetryAfter(Throwable $e): int\n {\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$e ' . PHP_EOL . print_r($e, true));\n return 5;\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 $policy = $this->parsePolicy($e);\n if ($policy === 'TEN_SECONDLY_ROLLING') {\n return 10;\n }\n if ($policy === 'SECONDLY') {\n return 1;\n }\n if ($policy === 'DAILY_LIMIT') {\n return 600;\n }\n\n $this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [\n 'exception_class' => get_class($e),\n ]);\n\n return 10;\n }\n\n public function parsePolicy(Throwable $e): ?string\n {\n if (! method_exists($e, 'getResponseBody')) {\n return null;\n }\n\n $body = $e->getResponseBody();\n if (is_string($body)) {\n $body = json_decode($body, true) ?? [];\n }\n\n if (! is_array($body)) {\n return null;\n }\n\n $policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;\n\n return is_string($policy) ? strtoupper($policy) : null;\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 * Execute a search request against HubSpot CRM objects with rate limiting.\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n * @return array The search response with 'results', 'total', 'paging' keys\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n */\n public function search(string $objectType, array $payload): array\n {\n $endpoint = self::BASE_URL . \"/crm/v3/objects/{$objectType}/search\";\n\n return $this->executeRequest(function () use ($endpoint, $payload) {\n $response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$response ' . PHP_EOL . print_r($response, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$response ' . PHP_EOL . print_r($response->getResponseHeaders(), true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$response ' . PHP_EOL . print_r($response->getHeaders(), true));\n\n return $response->toArray();\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 $deal = $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 return $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n return $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\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 (RateLimitException $e) {\n// throw $e;\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\\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 Illuminate\\Support\\Facades\\Redis;\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\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Reacts to a rate limits (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 $cacheKey = $this->getRateLimitCacheKey();\n\n $cachedRetryAfter = Redis::get($cacheKey);\n if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n (int) $cachedRetryAfter,\n );\n }\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);\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 'policy' => $this->parsePolicy($e),\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 getRateLimitCacheKey(): string\n {\n return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());\n }\n\n public function isHubspotRateLimit(Throwable $e): bool\n {\n if ($e instanceof BadRequest\n || $e instanceof DealApiException\n || $e instanceof ContactApiException\n || $e instanceof CompanyApiException\n || $e instanceof \\GuzzleHttp\\Exception\\RequestException\n ) {\n return (int) $e->getCode() === 429;\n }\n\n return false;\n }\n\n public function parseRetryAfter(Throwable $e): int\n {\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$e ' . PHP_EOL . print_r($e, true));\n return 5;\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 $policy = $this->parsePolicy($e);\n if ($policy === 'TEN_SECONDLY_ROLLING') {\n return 10;\n }\n if ($policy === 'SECONDLY') {\n return 1;\n }\n if ($policy === 'DAILY_LIMIT') {\n return 600;\n }\n\n $this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [\n 'exception_class' => get_class($e),\n ]);\n\n return 10;\n }\n\n public function parsePolicy(Throwable $e): ?string\n {\n if (! method_exists($e, 'getResponseBody')) {\n return null;\n }\n\n $body = $e->getResponseBody();\n if (is_string($body)) {\n $body = json_decode($body, true) ?? [];\n }\n\n if (! is_array($body)) {\n return null;\n }\n\n $policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;\n\n return is_string($policy) ? strtoupper($policy) : null;\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 * Execute a search request against HubSpot CRM objects with rate limiting.\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n * @return array The search response with 'results', 'total', 'paging' keys\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n */\n public function search(string $objectType, array $payload): array\n {\n $endpoint = self::BASE_URL . \"/crm/v3/objects/{$objectType}/search\";\n\n return $this->executeRequest(function () use ($endpoint, $payload) {\n $response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$response ' . PHP_EOL . print_r($response, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$response ' . PHP_EOL . print_r($response->getResponseHeaders(), true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$response ' . PHP_EOL . print_r($response->getHeaders(), true));\n\n return $response->toArray();\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 $deal = $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 return $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n return $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\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 (RateLimitException $e) {\n// throw $e;\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":"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":"19","depth":4,"bounds":{"left":0.96276593,"top":0.07581804,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.9740692,"top":0.074221864,"width":0.00731383,"height":0.018355945},"on_screen":true,"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.98138297,"top":0.074221864,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","depth":4,"bounds":{"left":0.61702126,"top":0.0726257,"width":0.38297874,"height":0.9066241},"on_screen":true,"lines":[{"char_start":273,"char_count":32,"bounds":{"left":0.61702126,"top":0.0,"width":0.080119684,"height":0.014365523}},{"char_start":305,"char_count":79,"bounds":{"left":0.61702126,"top":0.0,"width":0.20212767,"height":0.014365523}},{"char_start":384,"char_count":18,"bounds":{"left":0.61702126,"top":0.0,"width":0.043882977,"height":0.014365523}},{"char_start":402,"char_count":21,"bounds":{"left":0.61702126,"top":0.0,"width":0.051861703,"height":0.014365523}},{"char_start":423,"char_count":48,"bounds":{"left":0.61702126,"top":0.0,"width":0.12167553,"height":0.014365523}},{"char_start":471,"char_count":72,"bounds":{"left":0.61702126,"top":0.0015961692,"width":0.18384309,"height":0.014365523}},{"char_start":543,"char_count":40,"bounds":{"left":0.61702126,"top":0.01915403,"width":0.10106383,"height":0.014365523}},{"char_start":583,"char_count":41,"bounds":{"left":0.61702126,"top":0.03671189,"width":0.10372341,"height":0.014365523}},{"char_start":624,"char_count":72,"bounds":{"left":0.61702126,"top":0.054269753,"width":0.18384309,"height":0.014365523}},{"char_start":696,"char_count":219,"bounds":{"left":0.61702126,"top":0.07182761,"width":0.38297874,"height":0.014365523}},{"char_start":915,"char_count":83,"bounds":{"left":0.61702126,"top":0.08938547,"width":0.21243352,"height":0.014365523}},{"char_start":998,"char_count":20,"bounds":{"left":0.61702126,"top":0.10694334,"width":0.04920213,"height":0.014365523}},{"char_start":1018,"char_count":17,"bounds":{"left":0.61702126,"top":0.1245012,"width":0.041223403,"height":0.014365523}},{"char_start":1035,"char_count":203,"bounds":{"left":0.61702126,"top":0.14205906,"width":0.38297874,"height":0.014365523}},{"char_start":1238,"char_count":22,"bounds":{"left":0.61702126,"top":0.15961692,"width":0.05418883,"height":0.014365523}},{"char_start":1260,"char_count":23,"bounds":{"left":0.61702126,"top":0.17717478,"width":0.056848403,"height":0.014365523}},{"char_start":1283,"char_count":10,"bounds":{"left":0.61702126,"top":0.19473264,"width":0.023271276,"height":0.014365523}},{"char_start":1293,"char_count":27,"bounds":{"left":0.61702126,"top":0.2122905,"width":0.06715426,"height":0.014365523}},{"char_start":1320,"char_count":26,"bounds":{"left":0.61702126,"top":0.22984837,"width":0.06482713,"height":0.014365523}},{"char_start":1346,"char_count":23,"bounds":{"left":0.61702126,"top":0.24740623,"width":0.056848403,"height":0.014365523}},{"char_start":1369,"char_count":28,"bounds":{"left":0.61702126,"top":0.26496407,"width":0.06981383,"height":0.014365523}},{"char_start":1397,"char_count":57,"bounds":{"left":0.61702126,"top":0.28252193,"width":0.14494681,"height":0.014365523}}],"value":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","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}]...
|
4531694917553906031
|
6378618611371477348
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
4
71
3
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\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 Illuminate\Support\Facades\Redis;
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)
{
$cacheKey = $this->getRateLimitCacheKey();
$cachedRetryAfter = Redis::get($cacheKey);
if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {
throw new RateLimitException(
'Hubspot rate limit (cached circuit-breaker)',
(int) $cachedRetryAfter,
);
}
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'policy' => $this->parsePolicy($e),
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function getRateLimitCacheKey(): string
{
return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());
}
public function isHubspotRateLimit(Throwable $e): bool
{
if ($e instanceof BadRequest
|| $e instanceof DealApiException
|| $e instanceof ContactApiException
|| $e instanceof CompanyApiException
|| $e instanceof \GuzzleHttp\Exception\RequestException
) {
return (int) $e->getCode() === 429;
}
return false;
}
public function parseRetryAfter(Throwable $e): int
{
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$e ' . PHP_EOL . print_r($e, true));
return 5;
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;
}
}
$policy = $this->parsePolicy($e);
if ($policy === 'TEN_SECONDLY_ROLLING') {
return 10;
}
if ($policy === 'SECONDLY') {
return 1;
}
if ($policy === 'DAILY_LIMIT') {
return 600;
}
$this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [
'exception_class' => get_class($e),
]);
return 10;
}
public function parsePolicy(Throwable $e): ?string
{
if (! method_exists($e, 'getResponseBody')) {
return null;
}
$body = $e->getResponseBody();
if (is_string($body)) {
$body = json_decode($body, true) ?? [];
}
if (! is_array($body)) {
return null;
}
$policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;
return is_string($policy) ? strtoupper($policy) : null;
}
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
);
}
/**
* Execute a search request against HubSpot CRM objects with rate limiting.
*
* @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')
* @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.
* @return array The search response with 'results', 'total', 'paging' keys
* @throws RateLimitException When rate limit is hit
* @throws HubspotException On API errors
*/
public function search(string $objectType, array $payload): array
{
$endpoint = self::BASE_URL . "/crm/v3/objects/{$objectType}/search";
return $this->executeRequest(function () use ($endpoint, $payload) {
$response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$response ' . PHP_EOL . print_r($response, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$response ' . PHP_EOL . print_r($response->getResponseHeaders(), true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$response ' . PHP_EOL . print_r($response->getHeaders(), true));
return $response->toArray();
});
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$deal = $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') {
return $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
return $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
}
/**
* @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 (RateLimitException $e) {
// throw $e;
} 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);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
17949
|
NULL
|
NULL
|
NULL
|
|
17952
|
782
|
1
|
2026-05-11T10:42:57.206673+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778496177206_m1.jpg...
|
PhpStorm
|
faVsco.js – Client.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
SlackFileEditViewGoHistoryWindowHelpDOCKER2,5₴81DE SlackFileEditViewGoHistoryWindowHelpDOCKER2,5₴81DEV (docker)Last login: Sun May 10 21:10:26 on ttys013₴2APP (-zsh)883Poetry could not find a pyproject.toml file in /Users/lukas or its parentsPoetry could not find a pyproject.toml file in /Users/lukas or its parentslukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ 0-zshHomeDMsActivityFilesLater..•More(allED→Jiminny ...# conrusion-clinic# curiosity_lab# engineering# general# jiminny-bg# platform-tickets# product_launches# random# releases# sofia-office# support# thank-yous# the_people_of jimi...^ Direct messages®. Aneliya Angelova®. Galya Dimitrova& Petko Kashinski&. Stefka StoyanovaVasil Vasilev el&. Nikolay Ivanov3Aneliya Angelova, ...Stoyan Tanev• VesE Lukas Kovalik y... 0::: AppsS Jira CloudToastGanala CalaSupport Daily • in 1h 18 m100% <78• Mon 11 May 13:42:56Describe what you are looking forToastHomeMessagesAboutiminnyFriday, May 8th ~Added by Toast for GitHubToday ~NewToast APP10:00 AMReviewapp#12059 Jy 20820 es reindexstream model hydration2 days old • 12 files changed •@Vasil VasilevAdded by Toast for GitHubResolve Conflictsapp#11443 Test hublets latency5 months old • 20 files changed#11327 JY-19501 webhookbased opportunity syncShow moreAdded by Toast for GitHubNeeds Loveapp#12024 JY-20773 fix user pilottracking ofr automated reportgenerated12 days old • 1 file changedMessage Toast+Aa...
|
NULL
|
1095826402101641340
|
NULL
|
click
|
ocr
|
NULL
|
SlackFileEditViewGoHistoryWindowHelpDOCKER2,5₴81DE SlackFileEditViewGoHistoryWindowHelpDOCKER2,5₴81DEV (docker)Last login: Sun May 10 21:10:26 on ttys013₴2APP (-zsh)883Poetry could not find a pyproject.toml file in /Users/lukas or its parentsPoetry could not find a pyproject.toml file in /Users/lukas or its parentslukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ 0-zshHomeDMsActivityFilesLater..•More(allED→Jiminny ...# conrusion-clinic# curiosity_lab# engineering# general# jiminny-bg# platform-tickets# product_launches# random# releases# sofia-office# support# thank-yous# the_people_of jimi...^ Direct messages®. Aneliya Angelova®. Galya Dimitrova& Petko Kashinski&. Stefka StoyanovaVasil Vasilev el&. Nikolay Ivanov3Aneliya Angelova, ...Stoyan Tanev• VesE Lukas Kovalik y... 0::: AppsS Jira CloudToastGanala CalaSupport Daily • in 1h 18 m100% <78• Mon 11 May 13:42:56Describe what you are looking forToastHomeMessagesAboutiminnyFriday, May 8th ~Added by Toast for GitHubToday ~NewToast APP10:00 AMReviewapp#12059 Jy 20820 es reindexstream model hydration2 days old • 12 files changed •@Vasil VasilevAdded by Toast for GitHubResolve Conflictsapp#11443 Test hublets latency5 months old • 20 files changed#11327 JY-19501 webhookbased opportunity syncShow moreAdded by Toast for GitHubNeeds Loveapp#12024 JY-20773 fix user pilottracking ofr automated reportgenerated12 days old • 1 file changedMessage Toast+Aa...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
17953
|
783
|
1
|
2026-05-11T10:42:57.179154+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778496177179_m2.jpg...
|
PhpStorm
|
faVsco.js – Client.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
PhostormVIewINavicarecodeLaravelKeractorTOOISWindo PhostormVIewINavicarecodeLaravelKeractorTOOISWindowFV faVsco.js°9 JY-20725-handle-HS-search-rate-linroledey© PlaybackController.php© TrackRecordingFileSizT SyncCrmEntitiesTrait.phpCachedcrmservicebecorator.ong© VerifyActivityCrmTaskJob.php© TrackRecordingSizeEn© Job.phpT. ValidateSmitProspect:AjReportsBadkeouest.php© PaginationState.phgclass Cllent extends Baseclient 1mpLements Hubspotcllentintertace0 CalendarConference0 Crm230@ bullnorn0 CloseC Copper242)243244)>J Crmobiects_ DecorareAcuivilyDummy24612472481Helpersv h Hubspot2501AccountSvncStrate>D Actionsa ContactsvncStraterFields• Malournal256 C1 Metadatalv OpportunitySyncSt> O Concerns(c) Hubsnotl actMoC HubspotLastMo(C) Hubsnotl actMo© HubspotLastMo(C) Hubsnotl actMo(C) HubsnotSinaleS© HubspotSyncStr© HubspotWebhoo~ M Padination© HubspotPaginat© PaginationConfi© PaginationState> D ProspectSearchStr:› D Redisv D ServiceTraits() OpportunitvSvnc() SvncCrmEntitiesT SuncFieldstirait.T. WriteCrmTrait.o• M UtilsM WebhookC) BatchSvncCollectot(c) RatchSvncRedisSeClient nhr(C) ClocedDea|Stadecc@ DoalFieldsService rpublic function getPaginatedDataGenerator(Nwroo&. scotal,&: $lastRecordId* Execute a search request against Hubsoot cri obnects with rate limitina.* aparam strina Sobnecupe The obnect tupe 'deals', 'compannes'.'contacts', 'calls')@param array<string, mixed> $payload The search payload with filters, sorts, properties, etc* Greturn arrou The searchresconse withtotal'"naonnd"keus* Athrows RateLimitExcention When rate Limit is hit* athrows HubsnotExcentionOn APt errorspublic function search(string SobjectType, array Spayload): arraySendnnint = cpf••RASF IIRIneturn_Sthic->pyecuteRequest/function0uce (Sendnoint Snavloadh&Sresponse = $this->getInstance->getClient->request(rhod: 'POST', Sendpoint, ['json' => $payload]):Tlluminate Sunnont Facades Loa. • channel d cha'custom_channel')->info('$response' . PHP_EOL . print_r(Sresponse, return: true)):Tlluminate Sunnont Facades Loa. • channeld'custom_channel')->info('$response' . PHP_EOL . print_r(Sresponse->getHeaders•,return: toue)return Sresponse->toArrayO:})H* dchrows DealAp1Exceptzon* dchrows Crmexceptionpublic function getipportun1tyßvld(strina Scrmid. array Stields): arravtrySdeal = Sthis-teRequest(fn () => Sthis->qetNewInstance()->crm()->deals()->basicAni@->aetBvId(Sdeal = Sthis->aetNewinstance@->crmO->dealsO->basicAni@->getBvidimolode( separator:" ' Sfields)} catch (NealAniFycention Se) $Sthis->loa-sinfortfHuhsnot Sailed to fetch onnontunitvi.'crm_id' => $crmId,Se->ae+Meçcane O(C) HubSpot/Service.phpA RateLimitException.ong)PaginationConrig.pnpA3 A69 V3 1" suppont Dally • In 1h 10m100% LzP• Mon 11 May 13:42:57HandleHubspotRateLimitTest v« console (PROD]* console (EUl& console [STAGINGIw.19Alaravel.log4 SF [jiminny@localhost]& HS_local [jiminny@localhost][2026-05-07 14:21:15] local.INF0: [Hubspot] DEBUG Getting headers {"Date":["Thu,07 May 2026 14:21:15 GMT"],"Content-Type":["application/json;charset=utf-8"]."Transfer-Encoding": ["chunked"],"conneccion". Keep-alive"n"CF-Ray": ["9f80deb8db60dc3a-SOF"],"CF-Cache-Status":"DYNAMIC"]"Strict-Transport-Secur1ty":"max-aqe=31536000* 1ncludeSubDomains: preload")accept-encoding"],"access-control-allow-credentials": "false")."server-timing": ["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",cfn:desc="9-80de8ercodc3a-TAD""x-content-type-options": ["nosniff"]."x-hubsoot-correlation-id":"019e02d0-6fd8-7812-bdba-885b7ccb3ee3"]"Set-Cookie".[" cf bm=STUrtd0aXVrik50odaF6hZVYKhzTn0BidvMabeCtm0Y-1778163675-[IP_ADDRESS]-рT. ZaatDKxTae5zr8 2abBfWM00. ufZEXDZuHz2mBUFdzdo2aTHEs0)07-May-26 14:51:15 GMT; domain=.hubapi.com; Http0nly; Secure; SameSite=None"],"Renont-To"l."."endnoints"•!\"url\":\"https:|V/\V/a.nel.cloudflare.com/\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZLzoYdxI%2BIxVpHmsKn30%2BKVA3mFIJ2m7YRECDGS\"group\":\"cf-nel\",\"max_age\":604800}"],"success_fraction\":0.01,"report_to\":\"cf-nel\"\"max_age\":604800}"],"Server": ["cloudflare"]}} {"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab"."trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}W Windsurf Teams 263:129 UTF-8 f 4 spaces ®...
|
NULL
|
7067706998462025288
|
NULL
|
click
|
ocr
|
NULL
|
PhostormVIewINavicarecodeLaravelKeractorTOOISWindo PhostormVIewINavicarecodeLaravelKeractorTOOISWindowFV faVsco.js°9 JY-20725-handle-HS-search-rate-linroledey© PlaybackController.php© TrackRecordingFileSizT SyncCrmEntitiesTrait.phpCachedcrmservicebecorator.ong© VerifyActivityCrmTaskJob.php© TrackRecordingSizeEn© Job.phpT. ValidateSmitProspect:AjReportsBadkeouest.php© PaginationState.phgclass Cllent extends Baseclient 1mpLements Hubspotcllentintertace0 CalendarConference0 Crm230@ bullnorn0 CloseC Copper242)243244)>J Crmobiects_ DecorareAcuivilyDummy24612472481Helpersv h Hubspot2501AccountSvncStrate>D Actionsa ContactsvncStraterFields• Malournal256 C1 Metadatalv OpportunitySyncSt> O Concerns(c) Hubsnotl actMoC HubspotLastMo(C) Hubsnotl actMo© HubspotLastMo(C) Hubsnotl actMo(C) HubsnotSinaleS© HubspotSyncStr© HubspotWebhoo~ M Padination© HubspotPaginat© PaginationConfi© PaginationState> D ProspectSearchStr:› D Redisv D ServiceTraits() OpportunitvSvnc() SvncCrmEntitiesT SuncFieldstirait.T. WriteCrmTrait.o• M UtilsM WebhookC) BatchSvncCollectot(c) RatchSvncRedisSeClient nhr(C) ClocedDea|Stadecc@ DoalFieldsService rpublic function getPaginatedDataGenerator(Nwroo&. scotal,&: $lastRecordId* Execute a search request against Hubsoot cri obnects with rate limitina.* aparam strina Sobnecupe The obnect tupe 'deals', 'compannes'.'contacts', 'calls')@param array<string, mixed> $payload The search payload with filters, sorts, properties, etc* Greturn arrou The searchresconse withtotal'"naonnd"keus* Athrows RateLimitExcention When rate Limit is hit* athrows HubsnotExcentionOn APt errorspublic function search(string SobjectType, array Spayload): arraySendnnint = cpf••RASF IIRIneturn_Sthic->pyecuteRequest/function0uce (Sendnoint Snavloadh&Sresponse = $this->getInstance->getClient->request(rhod: 'POST', Sendpoint, ['json' => $payload]):Tlluminate Sunnont Facades Loa. • channel d cha'custom_channel')->info('$response' . PHP_EOL . print_r(Sresponse, return: true)):Tlluminate Sunnont Facades Loa. • channeld'custom_channel')->info('$response' . PHP_EOL . print_r(Sresponse->getHeaders•,return: toue)return Sresponse->toArrayO:})H* dchrows DealAp1Exceptzon* dchrows Crmexceptionpublic function getipportun1tyßvld(strina Scrmid. array Stields): arravtrySdeal = Sthis-teRequest(fn () => Sthis->qetNewInstance()->crm()->deals()->basicAni@->aetBvId(Sdeal = Sthis->aetNewinstance@->crmO->dealsO->basicAni@->getBvidimolode( separator:" ' Sfields)} catch (NealAniFycention Se) $Sthis->loa-sinfortfHuhsnot Sailed to fetch onnontunitvi.'crm_id' => $crmId,Se->ae+Meçcane O(C) HubSpot/Service.phpA RateLimitException.ong)PaginationConrig.pnpA3 A69 V3 1" suppont Dally • In 1h 10m100% LzP• Mon 11 May 13:42:57HandleHubspotRateLimitTest v« console (PROD]* console (EUl& console [STAGINGIw.19Alaravel.log4 SF [jiminny@localhost]& HS_local [jiminny@localhost][2026-05-07 14:21:15] local.INF0: [Hubspot] DEBUG Getting headers {"Date":["Thu,07 May 2026 14:21:15 GMT"],"Content-Type":["application/json;charset=utf-8"]."Transfer-Encoding": ["chunked"],"conneccion". Keep-alive"n"CF-Ray": ["9f80deb8db60dc3a-SOF"],"CF-Cache-Status":"DYNAMIC"]"Strict-Transport-Secur1ty":"max-aqe=31536000* 1ncludeSubDomains: preload")accept-encoding"],"access-control-allow-credentials": "false")."server-timing": ["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",cfn:desc="9-80de8ercodc3a-TAD""x-content-type-options": ["nosniff"]."x-hubsoot-correlation-id":"019e02d0-6fd8-7812-bdba-885b7ccb3ee3"]"Set-Cookie".[" cf bm=STUrtd0aXVrik50odaF6hZVYKhzTn0BidvMabeCtm0Y-1778163675-[IP_ADDRESS]-рT. ZaatDKxTae5zr8 2abBfWM00. ufZEXDZuHz2mBUFdzdo2aTHEs0)07-May-26 14:51:15 GMT; domain=.hubapi.com; Http0nly; Secure; SameSite=None"],"Renont-To"l."."endnoints"•!\"url\":\"https:|V/\V/a.nel.cloudflare.com/\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZLzoYdxI%2BIxVpHmsKn30%2BKVA3mFIJ2m7YRECDGS\"group\":\"cf-nel\",\"max_age\":604800}"],"success_fraction\":0.01,"report_to\":\"cf-nel\"\"max_age\":604800}"],"Server": ["cloudflare"]}} {"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab"."trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}W Windsurf Teams 263:129 UTF-8 f 4 spaces ®...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
17954
|
782
|
2
|
2026-05-11T10:43:02.487672+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778496182487_m1.jpg...
|
PhpStorm
|
faVsco.js – Client.php
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
3
69
4
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\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 Illuminate\Support\Facades\Redis;
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)
{
$cacheKey = $this->getRateLimitCacheKey();
$cachedRetryAfter = Redis::get($cacheKey);
if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {
throw new RateLimitException(
'Hubspot rate limit (cached circuit-breaker)',
(int) $cachedRetryAfter,
);
}
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'policy' => $this->parsePolicy($e),
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function getRateLimitCacheKey(): string
{
return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());
}
public function isHubspotRateLimit(Throwable $e): bool
{
if ($e instanceof BadRequest
|| $e instanceof DealApiException
|| $e instanceof ContactApiException
|| $e instanceof CompanyApiException
|| $e instanceof \GuzzleHttp\Exception\RequestException
) {
return (int) $e->getCode() === 429;
}
return false;
}
public function parseRetryAfter(Throwable $e): int
{
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$e ' . PHP_EOL . print_r($e, true));
return 5;
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;
}
}
$policy = $this->parsePolicy($e);
if ($policy === 'TEN_SECONDLY_ROLLING') {
return 10;
}
if ($policy === 'SECONDLY') {
return 1;
}
if ($policy === 'DAILY_LIMIT') {
return 600;
}
$this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [
'exception_class' => get_class($e),
]);
return 10;
}
public function parsePolicy(Throwable $e): ?string
{
if (! method_exists($e, 'getResponseBody')) {
return null;
}
$body = $e->getResponseBody();
if (is_string($body)) {
$body = json_decode($body, true) ?? [];
}
if (! is_array($body)) {
return null;
}
$policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;
return is_string($policy) ? strtoupper($policy) : null;
}
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
);
}
/**
* Execute a search request against HubSpot CRM objects with rate limiting.
*
* @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')
* @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.
* @return array The search response with 'results', 'total', 'paging' keys
* @throws RateLimitException When rate limit is hit
* @throws HubspotException On API errors
*/
public function search(string $objectType, array $payload): array
{
$endpoint = self::BASE_URL . "/crm/v3/objects/{$objectType}/search";
return $this->executeRequest(function () use ($endpoint, $payload) {
$response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$response ' . PHP_EOL . print_r($response, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$headers ' . PHP_EOL . print_r($response->getHeaders(), true));
return $response->toArray();
});
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$deal = $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') {
return $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
return $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
}
/**
* @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 (RateLimitException $e) {
// throw $e;
} 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);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
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":"JY-20725-handle-HS-search-rate-limit, menu","depth":5,"on_screen":true,"help_text":"Git Branch: JY-20725-handle-HS-search-rate-limit","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":"HandleHubspotRateLimitTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'HandleHubspotRateLimitTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'HandleHubspotRateLimitTest'","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":"AXStaticText","text":"3","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"69","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"4","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"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\\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 Illuminate\\Support\\Facades\\Redis;\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\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Reacts to a rate limits (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 $cacheKey = $this->getRateLimitCacheKey();\n\n $cachedRetryAfter = Redis::get($cacheKey);\n if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n (int) $cachedRetryAfter,\n );\n }\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);\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 'policy' => $this->parsePolicy($e),\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 getRateLimitCacheKey(): string\n {\n return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());\n }\n\n public function isHubspotRateLimit(Throwable $e): bool\n {\n if ($e instanceof BadRequest\n || $e instanceof DealApiException\n || $e instanceof ContactApiException\n || $e instanceof CompanyApiException\n || $e instanceof \\GuzzleHttp\\Exception\\RequestException\n ) {\n return (int) $e->getCode() === 429;\n }\n\n return false;\n }\n\n public function parseRetryAfter(Throwable $e): int\n {\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$e ' . PHP_EOL . print_r($e, true));\n return 5;\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 $policy = $this->parsePolicy($e);\n if ($policy === 'TEN_SECONDLY_ROLLING') {\n return 10;\n }\n if ($policy === 'SECONDLY') {\n return 1;\n }\n if ($policy === 'DAILY_LIMIT') {\n return 600;\n }\n\n $this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [\n 'exception_class' => get_class($e),\n ]);\n\n return 10;\n }\n\n public function parsePolicy(Throwable $e): ?string\n {\n if (! method_exists($e, 'getResponseBody')) {\n return null;\n }\n\n $body = $e->getResponseBody();\n if (is_string($body)) {\n $body = json_decode($body, true) ?? [];\n }\n\n if (! is_array($body)) {\n return null;\n }\n\n $policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;\n\n return is_string($policy) ? strtoupper($policy) : null;\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 * Execute a search request against HubSpot CRM objects with rate limiting.\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n * @return array The search response with 'results', 'total', 'paging' keys\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n */\n public function search(string $objectType, array $payload): array\n {\n $endpoint = self::BASE_URL . \"/crm/v3/objects/{$objectType}/search\";\n\n return $this->executeRequest(function () use ($endpoint, $payload) {\n $response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$response ' . PHP_EOL . print_r($response, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$headers ' . PHP_EOL . print_r($response->getHeaders(), true));\n\n return $response->toArray();\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 $deal = $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 return $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n return $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\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 (RateLimitException $e) {\n// throw $e;\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\\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 Illuminate\\Support\\Facades\\Redis;\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\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Reacts to a rate limits (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 $cacheKey = $this->getRateLimitCacheKey();\n\n $cachedRetryAfter = Redis::get($cacheKey);\n if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n (int) $cachedRetryAfter,\n );\n }\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);\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 'policy' => $this->parsePolicy($e),\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 getRateLimitCacheKey(): string\n {\n return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());\n }\n\n public function isHubspotRateLimit(Throwable $e): bool\n {\n if ($e instanceof BadRequest\n || $e instanceof DealApiException\n || $e instanceof ContactApiException\n || $e instanceof CompanyApiException\n || $e instanceof \\GuzzleHttp\\Exception\\RequestException\n ) {\n return (int) $e->getCode() === 429;\n }\n\n return false;\n }\n\n public function parseRetryAfter(Throwable $e): int\n {\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$e ' . PHP_EOL . print_r($e, true));\n return 5;\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 $policy = $this->parsePolicy($e);\n if ($policy === 'TEN_SECONDLY_ROLLING') {\n return 10;\n }\n if ($policy === 'SECONDLY') {\n return 1;\n }\n if ($policy === 'DAILY_LIMIT') {\n return 600;\n }\n\n $this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [\n 'exception_class' => get_class($e),\n ]);\n\n return 10;\n }\n\n public function parsePolicy(Throwable $e): ?string\n {\n if (! method_exists($e, 'getResponseBody')) {\n return null;\n }\n\n $body = $e->getResponseBody();\n if (is_string($body)) {\n $body = json_decode($body, true) ?? [];\n }\n\n if (! is_array($body)) {\n return null;\n }\n\n $policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;\n\n return is_string($policy) ? strtoupper($policy) : null;\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 * Execute a search request against HubSpot CRM objects with rate limiting.\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n * @return array The search response with 'results', 'total', 'paging' keys\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n */\n public function search(string $objectType, array $payload): array\n {\n $endpoint = self::BASE_URL . \"/crm/v3/objects/{$objectType}/search\";\n\n return $this->executeRequest(function () use ($endpoint, $payload) {\n $response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$response ' . PHP_EOL . print_r($response, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$headers ' . PHP_EOL . print_r($response->getHeaders(), true));\n\n return $response->toArray();\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 $deal = $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 return $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n return $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\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 (RateLimitException $e) {\n// throw $e;\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":"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":"19","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","depth":4,"on_screen":true,"value":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","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}]...
|
3084131163382364615
|
6378618611371477092
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
3
69
4
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\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 Illuminate\Support\Facades\Redis;
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)
{
$cacheKey = $this->getRateLimitCacheKey();
$cachedRetryAfter = Redis::get($cacheKey);
if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {
throw new RateLimitException(
'Hubspot rate limit (cached circuit-breaker)',
(int) $cachedRetryAfter,
);
}
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'policy' => $this->parsePolicy($e),
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function getRateLimitCacheKey(): string
{
return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());
}
public function isHubspotRateLimit(Throwable $e): bool
{
if ($e instanceof BadRequest
|| $e instanceof DealApiException
|| $e instanceof ContactApiException
|| $e instanceof CompanyApiException
|| $e instanceof \GuzzleHttp\Exception\RequestException
) {
return (int) $e->getCode() === 429;
}
return false;
}
public function parseRetryAfter(Throwable $e): int
{
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$e ' . PHP_EOL . print_r($e, true));
return 5;
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;
}
}
$policy = $this->parsePolicy($e);
if ($policy === 'TEN_SECONDLY_ROLLING') {
return 10;
}
if ($policy === 'SECONDLY') {
return 1;
}
if ($policy === 'DAILY_LIMIT') {
return 600;
}
$this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [
'exception_class' => get_class($e),
]);
return 10;
}
public function parsePolicy(Throwable $e): ?string
{
if (! method_exists($e, 'getResponseBody')) {
return null;
}
$body = $e->getResponseBody();
if (is_string($body)) {
$body = json_decode($body, true) ?? [];
}
if (! is_array($body)) {
return null;
}
$policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;
return is_string($policy) ? strtoupper($policy) : null;
}
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
);
}
/**
* Execute a search request against HubSpot CRM objects with rate limiting.
*
* @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')
* @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.
* @return array The search response with 'results', 'total', 'paging' keys
* @throws RateLimitException When rate limit is hit
* @throws HubspotException On API errors
*/
public function search(string $objectType, array $payload): array
{
$endpoint = self::BASE_URL . "/crm/v3/objects/{$objectType}/search";
return $this->executeRequest(function () use ($endpoint, $payload) {
$response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$response ' . PHP_EOL . print_r($response, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$headers ' . PHP_EOL . print_r($response->getHeaders(), true));
return $response->toArray();
});
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$deal = $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') {
return $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
return $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
}
/**
* @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 (RateLimitException $e) {
// throw $e;
} 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);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
17952
|
NULL
|
NULL
|
NULL
|
|
17955
|
783
|
2
|
2026-05-11T10:43:02.487714+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778496182487_m2.jpg...
|
PhpStorm
|
faVsco.js – Client.php
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
3
69
4
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\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 Illuminate\Support\Facades\Redis;
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)
{
$cacheKey = $this->getRateLimitCacheKey();
$cachedRetryAfter = Redis::get($cacheKey);
if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {
throw new RateLimitException(
'Hubspot rate limit (cached circuit-breaker)',
(int) $cachedRetryAfter,
);
}
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'policy' => $this->parsePolicy($e),
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function getRateLimitCacheKey(): string
{
return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());
}
public function isHubspotRateLimit(Throwable $e): bool
{
if ($e instanceof BadRequest
|| $e instanceof DealApiException
|| $e instanceof ContactApiException
|| $e instanceof CompanyApiException
|| $e instanceof \GuzzleHttp\Exception\RequestException
) {
return (int) $e->getCode() === 429;
}
return false;
}
public function parseRetryAfter(Throwable $e): int
{
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$e ' . PHP_EOL . print_r($e, true));
return 5;
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;
}
}
$policy = $this->parsePolicy($e);
if ($policy === 'TEN_SECONDLY_ROLLING') {
return 10;
}
if ($policy === 'SECONDLY') {
return 1;
}
if ($policy === 'DAILY_LIMIT') {
return 600;
}
$this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [
'exception_class' => get_class($e),
]);
return 10;
}
public function parsePolicy(Throwable $e): ?string
{
if (! method_exists($e, 'getResponseBody')) {
return null;
}
$body = $e->getResponseBody();
if (is_string($body)) {
$body = json_decode($body, true) ?? [];
}
if (! is_array($body)) {
return null;
}
$policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;
return is_string($policy) ? strtoupper($policy) : null;
}
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
);
}
/**
* Execute a search request against HubSpot CRM objects with rate limiting.
*
* @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')
* @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.
* @return array The search response with 'results', 'total', 'paging' keys
* @throws RateLimitException When rate limit is hit
* @throws HubspotException On API errors
*/
public function search(string $objectType, array $payload): array
{
$endpoint = self::BASE_URL . "/crm/v3/objects/{$objectType}/search";
return $this->executeRequest(function () use ($endpoint, $payload) {
$response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$response ' . PHP_EOL . print_r($response, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$headers ' . PHP_EOL . print_r($response->getHeaders(), true));
return $response->toArray();
});
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$deal = $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') {
return $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
return $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
}
/**
* @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 (RateLimitException $e) {
// throw $e;
} 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);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
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":"JY-20725-handle-HS-search-rate-limit, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.09541223,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: JY-20725-handle-HS-search-rate-limit","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.82413566,"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":"HandleHubspotRateLimitTest","depth":6,"bounds":{"left":0.8394282,"top":0.019952115,"width":0.076130316,"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 'HandleHubspotRateLimitTest'","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 'HandleHubspotRateLimitTest'","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":"AXStaticText","text":"3","depth":4,"bounds":{"left":0.5515292,"top":0.15003991,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"69","depth":4,"bounds":{"left":0.56150264,"top":0.15003991,"width":0.010305851,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"4","depth":4,"bounds":{"left":0.5738032,"top":0.15003991,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.5834442,"top":0.14844373,"width":0.00731383,"height":0.018355945},"on_screen":true,"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.59075797,"top":0.14844373,"width":0.006981383,"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\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\\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 Illuminate\\Support\\Facades\\Redis;\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\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Reacts to a rate limits (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 $cacheKey = $this->getRateLimitCacheKey();\n\n $cachedRetryAfter = Redis::get($cacheKey);\n if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n (int) $cachedRetryAfter,\n );\n }\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);\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 'policy' => $this->parsePolicy($e),\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 getRateLimitCacheKey(): string\n {\n return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());\n }\n\n public function isHubspotRateLimit(Throwable $e): bool\n {\n if ($e instanceof BadRequest\n || $e instanceof DealApiException\n || $e instanceof ContactApiException\n || $e instanceof CompanyApiException\n || $e instanceof \\GuzzleHttp\\Exception\\RequestException\n ) {\n return (int) $e->getCode() === 429;\n }\n\n return false;\n }\n\n public function parseRetryAfter(Throwable $e): int\n {\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$e ' . PHP_EOL . print_r($e, true));\n return 5;\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 $policy = $this->parsePolicy($e);\n if ($policy === 'TEN_SECONDLY_ROLLING') {\n return 10;\n }\n if ($policy === 'SECONDLY') {\n return 1;\n }\n if ($policy === 'DAILY_LIMIT') {\n return 600;\n }\n\n $this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [\n 'exception_class' => get_class($e),\n ]);\n\n return 10;\n }\n\n public function parsePolicy(Throwable $e): ?string\n {\n if (! method_exists($e, 'getResponseBody')) {\n return null;\n }\n\n $body = $e->getResponseBody();\n if (is_string($body)) {\n $body = json_decode($body, true) ?? [];\n }\n\n if (! is_array($body)) {\n return null;\n }\n\n $policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;\n\n return is_string($policy) ? strtoupper($policy) : null;\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 * Execute a search request against HubSpot CRM objects with rate limiting.\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n * @return array The search response with 'results', 'total', 'paging' keys\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n */\n public function search(string $objectType, array $payload): array\n {\n $endpoint = self::BASE_URL . \"/crm/v3/objects/{$objectType}/search\";\n\n return $this->executeRequest(function () use ($endpoint, $payload) {\n $response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$response ' . PHP_EOL . print_r($response, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$headers ' . PHP_EOL . print_r($response->getHeaders(), true));\n\n return $response->toArray();\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 $deal = $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 return $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n return $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\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 (RateLimitException $e) {\n// throw $e;\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\\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 Illuminate\\Support\\Facades\\Redis;\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\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Reacts to a rate limits (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 $cacheKey = $this->getRateLimitCacheKey();\n\n $cachedRetryAfter = Redis::get($cacheKey);\n if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n (int) $cachedRetryAfter,\n );\n }\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);\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 'policy' => $this->parsePolicy($e),\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 getRateLimitCacheKey(): string\n {\n return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());\n }\n\n public function isHubspotRateLimit(Throwable $e): bool\n {\n if ($e instanceof BadRequest\n || $e instanceof DealApiException\n || $e instanceof ContactApiException\n || $e instanceof CompanyApiException\n || $e instanceof \\GuzzleHttp\\Exception\\RequestException\n ) {\n return (int) $e->getCode() === 429;\n }\n\n return false;\n }\n\n public function parseRetryAfter(Throwable $e): int\n {\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$e ' . PHP_EOL . print_r($e, true));\n return 5;\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 $policy = $this->parsePolicy($e);\n if ($policy === 'TEN_SECONDLY_ROLLING') {\n return 10;\n }\n if ($policy === 'SECONDLY') {\n return 1;\n }\n if ($policy === 'DAILY_LIMIT') {\n return 600;\n }\n\n $this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [\n 'exception_class' => get_class($e),\n ]);\n\n return 10;\n }\n\n public function parsePolicy(Throwable $e): ?string\n {\n if (! method_exists($e, 'getResponseBody')) {\n return null;\n }\n\n $body = $e->getResponseBody();\n if (is_string($body)) {\n $body = json_decode($body, true) ?? [];\n }\n\n if (! is_array($body)) {\n return null;\n }\n\n $policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;\n\n return is_string($policy) ? strtoupper($policy) : null;\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 * Execute a search request against HubSpot CRM objects with rate limiting.\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n * @return array The search response with 'results', 'total', 'paging' keys\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n */\n public function search(string $objectType, array $payload): array\n {\n $endpoint = self::BASE_URL . \"/crm/v3/objects/{$objectType}/search\";\n\n return $this->executeRequest(function () use ($endpoint, $payload) {\n $response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$response ' . PHP_EOL . print_r($response, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$headers ' . PHP_EOL . print_r($response->getHeaders(), true));\n\n return $response->toArray();\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 $deal = $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 return $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n return $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\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 (RateLimitException $e) {\n// throw $e;\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":"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":"19","depth":4,"bounds":{"left":0.96276593,"top":0.07581804,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.9740692,"top":0.074221864,"width":0.00731383,"height":0.018355945},"on_screen":true,"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.98138297,"top":0.074221864,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","depth":4,"bounds":{"left":0.61702126,"top":0.0726257,"width":0.38297874,"height":0.9066241},"on_screen":true,"lines":[{"char_start":273,"char_count":32,"bounds":{"left":0.61702126,"top":0.0,"width":0.080119684,"height":0.014365523}},{"char_start":305,"char_count":79,"bounds":{"left":0.61702126,"top":0.0,"width":0.20212767,"height":0.014365523}},{"char_start":384,"char_count":18,"bounds":{"left":0.61702126,"top":0.0,"width":0.043882977,"height":0.014365523}},{"char_start":402,"char_count":21,"bounds":{"left":0.61702126,"top":0.0,"width":0.051861703,"height":0.014365523}},{"char_start":423,"char_count":48,"bounds":{"left":0.61702126,"top":0.0,"width":0.12167553,"height":0.014365523}},{"char_start":471,"char_count":72,"bounds":{"left":0.61702126,"top":0.0015961692,"width":0.18384309,"height":0.014365523}},{"char_start":543,"char_count":40,"bounds":{"left":0.61702126,"top":0.01915403,"width":0.10106383,"height":0.014365523}},{"char_start":583,"char_count":41,"bounds":{"left":0.61702126,"top":0.03671189,"width":0.10372341,"height":0.014365523}},{"char_start":624,"char_count":72,"bounds":{"left":0.61702126,"top":0.054269753,"width":0.18384309,"height":0.014365523}},{"char_start":696,"char_count":219,"bounds":{"left":0.61702126,"top":0.07182761,"width":0.38297874,"height":0.014365523}},{"char_start":915,"char_count":83,"bounds":{"left":0.61702126,"top":0.08938547,"width":0.21243352,"height":0.014365523}},{"char_start":998,"char_count":20,"bounds":{"left":0.61702126,"top":0.10694334,"width":0.04920213,"height":0.014365523}},{"char_start":1018,"char_count":17,"bounds":{"left":0.61702126,"top":0.1245012,"width":0.041223403,"height":0.014365523}},{"char_start":1035,"char_count":203,"bounds":{"left":0.61702126,"top":0.14205906,"width":0.38297874,"height":0.014365523}},{"char_start":1238,"char_count":22,"bounds":{"left":0.61702126,"top":0.15961692,"width":0.05418883,"height":0.014365523}},{"char_start":1260,"char_count":23,"bounds":{"left":0.61702126,"top":0.17717478,"width":0.056848403,"height":0.014365523}},{"char_start":1283,"char_count":10,"bounds":{"left":0.61702126,"top":0.19473264,"width":0.023271276,"height":0.014365523}},{"char_start":1293,"char_count":27,"bounds":{"left":0.61702126,"top":0.2122905,"width":0.06715426,"height":0.014365523}},{"char_start":1320,"char_count":26,"bounds":{"left":0.61702126,"top":0.22984837,"width":0.06482713,"height":0.014365523}},{"char_start":1346,"char_count":23,"bounds":{"left":0.61702126,"top":0.24740623,"width":0.056848403,"height":0.014365523}},{"char_start":1369,"char_count":28,"bounds":{"left":0.61702126,"top":0.26496407,"width":0.06981383,"height":0.014365523}},{"char_start":1397,"char_count":57,"bounds":{"left":0.61702126,"top":0.28252193,"width":0.14494681,"height":0.014365523}}],"value":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","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}]...
|
3084131163382364615
|
6378618611371477092
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
3
69
4
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\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 Illuminate\Support\Facades\Redis;
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)
{
$cacheKey = $this->getRateLimitCacheKey();
$cachedRetryAfter = Redis::get($cacheKey);
if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {
throw new RateLimitException(
'Hubspot rate limit (cached circuit-breaker)',
(int) $cachedRetryAfter,
);
}
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'policy' => $this->parsePolicy($e),
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function getRateLimitCacheKey(): string
{
return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());
}
public function isHubspotRateLimit(Throwable $e): bool
{
if ($e instanceof BadRequest
|| $e instanceof DealApiException
|| $e instanceof ContactApiException
|| $e instanceof CompanyApiException
|| $e instanceof \GuzzleHttp\Exception\RequestException
) {
return (int) $e->getCode() === 429;
}
return false;
}
public function parseRetryAfter(Throwable $e): int
{
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$e ' . PHP_EOL . print_r($e, true));
return 5;
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;
}
}
$policy = $this->parsePolicy($e);
if ($policy === 'TEN_SECONDLY_ROLLING') {
return 10;
}
if ($policy === 'SECONDLY') {
return 1;
}
if ($policy === 'DAILY_LIMIT') {
return 600;
}
$this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [
'exception_class' => get_class($e),
]);
return 10;
}
public function parsePolicy(Throwable $e): ?string
{
if (! method_exists($e, 'getResponseBody')) {
return null;
}
$body = $e->getResponseBody();
if (is_string($body)) {
$body = json_decode($body, true) ?? [];
}
if (! is_array($body)) {
return null;
}
$policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;
return is_string($policy) ? strtoupper($policy) : null;
}
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
);
}
/**
* Execute a search request against HubSpot CRM objects with rate limiting.
*
* @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')
* @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.
* @return array The search response with 'results', 'total', 'paging' keys
* @throws RateLimitException When rate limit is hit
* @throws HubspotException On API errors
*/
public function search(string $objectType, array $payload): array
{
$endpoint = self::BASE_URL . "/crm/v3/objects/{$objectType}/search";
return $this->executeRequest(function () use ($endpoint, $payload) {
$response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$response ' . PHP_EOL . print_r($response, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$headers ' . PHP_EOL . print_r($response->getHeaders(), true));
return $response->toArray();
});
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$deal = $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') {
return $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
return $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
}
/**
* @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 (RateLimitException $e) {
// throw $e;
} 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);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
17953
|
NULL
|
NULL
|
NULL
|
|
17960
|
782
|
5
|
2026-05-11T10:43:16.163087+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778496196163_m1.jpg...
|
PhpStorm
|
faVsco.js – custom.log
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu...
|
[{"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":"JY-20725-handle-HS-search-rate-limit, menu","depth":5,"on_screen":true,"help_text":"Git Branch: JY-20725-handle-HS-search-rate-limit","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-5641617897080429754
|
-8160223333407913180
|
click
|
hybrid
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
iTerm2ShellEditViewSessionScriptsProfilesWindowHelp•DOCKER₴81DEV (docker)APP (-zsh)#3viewsjiminny-worker-processing-2:jiminny-worker-processing-2_00: stoppedjiminny-worker-processing-3:jiminny-worker-processing-3_00: stoppedjiminny-worker-processing-4:jiminny-worker-processing-4_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-conferences:worker-conferences_00: stoppedworker-crm-update:worker-crm-update_00: stoppedworker-download:worker-download_00: stoppedworker-emails:worker-emails_00: stoppedworker-nudges:worker-nudges_00: stoppedworker:worker_00: stoppedworker-audio:worker-audio_00: stoppedworker-calendar:worker-calendar_00: stoppedworker-crm-sync:worker-crm-sync_00: stoppedartisan-schedule:artisan-schedule_00: stoppedworker-es-update:worker-es-update_00: stoppedjiminny-worker-processing-1: jiminny-worker-processing-1_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 jiminny: debugDispatching 100 MatchActivityCrmData jobs (portal=2)Done. Watch logs and run jiminny:debug observeRateLimit to inspect cache state.root@docker_lamp_1:/home/jiminny# php artisan jiminny:debugDispatching 100 MatchActivityCrmData jobs (portal=2)Done. Watch logs and run jiminny:debug observeRateLimit to inspect cache state.root@docker_lamp_1:/home/jiminny#<DEV (docker)-zshSupport Daily • in 1h 17 m84-zsh*5100% C8 • Mon 11 May 13:43:15screenpipe"O ₴65.46ms DONE-zshT81*7+DEV...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
17961
|
783
|
5
|
2026-05-11T10:43:16.135871+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778496196135_m2.jpg...
|
PhpStorm
|
faVsco.js – custom.log
|
1
|
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
|
click
|
hybrid
|
NULL
|
Project: faVsco.js, menu
PnostormcodeFV faVsco.js° Project: faVsco.js, menu
PnostormcodeFV faVsco.js°9 JY-20725-handle-HS-searchroledeyC) TrackAutomated ReportGeneratedzventonp© UserAutomatedReportsController.php© PlaybackController.php© TrackRecordingFileSizT SyncCrmEntitiesTrait.phpC) CachedCrmServiceDecorator.ongT DeleteCrmEntityTrait.php© VerifyActivityCrmTaskJob.php© TrackRecordingSizeEn@ RematchActiT. ValidateSmitProspect:leenecknaik© Job.phpAjReports© HandleHub© BadReuest.phpC) Kernel.php© PaginationState.phg0 CalendarConference0 Crm@ bullnorn0 Close0 Copper230242243244)>J Crmobiects_ DecorareAcuivilyDummy24612472481Helpersv h Hubspot2501AccountSvncStrate>D Actionsa ContactsuncStratedm Fields• M lournal256 01 Metadatalv OpportunitySyncSt> O Concerns(c) Hubsnotl actMoC HubspotLastMo(C) Hubsnotl actMo© HubspotLastMo(C) Hubsnotl actMo(C) HubsnotSinaleS© HubspotSyncStr© HubspotWebhoo~ M Padination© HubspotPaginat© PaginationConfi(C) PaqinationState> D ProspectSearchStr:› D Redisv D ServiceTraits() OpportunitvSvnc() SvncCrmEntitiesT SuncFieldstirait.T. WriteCrmTrait.o• M UtilsM WebhookC) BatchSvncCollectot(c) RatchSvncRedisSec) Client nhr(C) ClocedDea|Stadecc@ DoalFieldsService rclass Cllent extends Baseclient 1mpLements Hubspotcllentintertacepublic function getPaginatedDataGenerator(Nwroo&. scotal,&: $lastRecordId* Execute a search request against Hubspot CRM objects with rate limiting.* aparam strina Sobnecupe The obnect tupe 'deals', 'compannes'.'contacts', 'calls')@param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.* Greturn arrou The searchresconse with "results'."total", "naonnd' keus* Athrows RateLimitExcention When rate Limit is hit* athrows HubsnotExcentionOn APt errorspublic function search(string SobjectType, array $payload): arraySendnnint = cpf••RASF IIRIIneturn_Sthic->pyecuteRequest/function0uce (Sendnoint Snavloadh&Sresponse = $this->getInstance->getClient->request(mthod: "POST', Sendpoint, ['json' => $payload]):l\Illuminate\Support\Facades \Log::channel( cha'custom_channel')->info('$response' . PHP_EOL . print_r(Sresponse,return: true)):Tlluminate Sunnont Facades Loa. • channel d'custom_channel')->info('$headers' . PHP_EOL . print_r(Sresponse->getHeaders,return: true));return Sresponse->toArrayO:* dchrows DealAp1Exceptzon* dchrows Crmexceptionpublic function getupportun1tyßvld(strina Scrmid. array Stields): arravtrySdeal = Sthis-teRequest(fn () => Sthis->qetNewInstance()->crm()->deals()->basicApi@->aetBvId(Sdeal = Sthis->aetNewinstance@->crmO->dealsO->basicAni@->getBvidimolode( separator:" ' Sfields)} catch (NealAniFycention Se) $Sthis->loa-sinfortfHuhsnot Sailed to fetch onnontunitv!.'crm_id' => $crmId,Se->ae+Meçcane O(C) HubSpot/Service.phpA RateLimitException.ong© PaginationConfig.phpA3 A70 V3 A‹ <0 hullsupoont Dally• In 1n 1/m100% • Mon 11 May 13.43.10laravel.log4 SF [jiminny@localhost]& HS_local [jiminny@localhost]« console (PROD]* console (EUl& console (STAGINGI[2026-05-07 14:21:15] local.INF0: [Hubspot] DEBUG Getting headers {w.19A"Date":["Thu,07 May 2026 14:21:15 GMT"],"Content-Type":["application/json;charset=utf-8"]."Transfer-Encoding": ["chunked"],"conneccion". Keep-alive"n"CF-Ray" : ["9f80deb8db60dc3a-SOF"],"CF-Cache-Status":"DYNAMIC"]"Strict-Transport-Secur1ty":"max-aqe=31536000* 1ncludeSubDomains: preload")accept-encoding"],"access-control-allow-credentials": "false")"server-timing": ["hcid; desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",cfn:desc="9-80de8ercodcsa-TAD"'."x-content-type-options": ["nosniff"]."x-hubsoot-correlation-id":"019e02d0-6fd8-7812-bdba-885b7ccb3ee3"]"Set-Cookie".[" cf bm=STUrtd0aXVrik50odaF6hZVYKhzTn0BidvMabeCtm0Y-1778163675-[IP_ADDRESS]-рT. ZaatDKxTae5zr8 2abBfWM00. ufZEXDZuHz2mBUFdzdo2aTHEs0)07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],"Renont-To"l.|","endnoints"•!\"url\":\"https:|V/\V/a.nel.cloudflare.com/\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZLzoYdxI%2BIxVpHmsKn30%2BKVA3mFIJ2m7YRECDGS\"group\":\"cf-nel\",\"max_age\":604800}"],"success_fraction\":0.01,"report_to\":\"cf-nel\"\"max_age\":604800}"],"Server": ["cloudflare"]}} {"correlation id":"95236535-ec98-4541-b92a-adfa73b69eab"."trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}fo 4 spaces 0...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
17962
|
782
|
6
|
2026-05-11T10:43:18.543358+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778496198543_m1.jpg...
|
PhpStorm
|
faVsco.js – custom.log
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
iTerm2ShellEdit|ViewSessionScriptsProfilesWindowHe iTerm2ShellEdit|ViewSessionScriptsProfilesWindowHelpDOCKER© *1DEV (docker)$2APP (-zsh)H3viewsjiminny-worker-processing-2:jiminny-worker-processing-2_00: stoppedjiminny-worker-processing-3:jiminny-worker-processing-3_00: stoppedjiminny-worker-processing-4:jiminny-worker-processing-4_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-conferences:worker-conferences_00: stoppedworker-crm-update:worker-crm-update_00: stoppedworker-download:worker-download_00: stoppedworker-emails:worker-emails_00: stoppedworker-nudges:worker-nudges_00: stoppedworker:worker_00: stoppedworker-audio:worker-audio_00: stoppedworker-calendar:worker-calendar_00: stoppedworker-crm-sync:worker-crm-sync_00: stoppedartisan-schedule:artisan-schedule_00: stoppedworker-es-update:worker-es-update_00: stoppedjiminny-worker-processing-1: jiminny-worker-processing-1_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 jiminny: debugDispatching 100 MatchActivityCrmData jobs (portal=2)Done. Watch logs and run jiminny:debug observeRateLimit to inspect cache state.root@docker_lamp_1:/home/jiminny# php artisan jiminny:debugDispatching 100 MatchActivityCrmData jobs (portal=2)Done. Watch logs and run jiminny:debug observeRateLimit to inspect cache state.root@docker_lamp_1:/home/jiminny# ]ld6]Support Daily - in 1h 17 m100% <78• Mon 11 May 13:43:18T₴1DEV (docker)-zsh84-zshX5screenpipe"2 8665.46ms DONE-zshDEV...
|
NULL
|
-8451160521887630771
|
NULL
|
visual_change
|
ocr
|
NULL
|
iTerm2ShellEdit|ViewSessionScriptsProfilesWindowHe iTerm2ShellEdit|ViewSessionScriptsProfilesWindowHelpDOCKER© *1DEV (docker)$2APP (-zsh)H3viewsjiminny-worker-processing-2:jiminny-worker-processing-2_00: stoppedjiminny-worker-processing-3:jiminny-worker-processing-3_00: stoppedjiminny-worker-processing-4:jiminny-worker-processing-4_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-conferences:worker-conferences_00: stoppedworker-crm-update:worker-crm-update_00: stoppedworker-download:worker-download_00: stoppedworker-emails:worker-emails_00: stoppedworker-nudges:worker-nudges_00: stoppedworker:worker_00: stoppedworker-audio:worker-audio_00: stoppedworker-calendar:worker-calendar_00: stoppedworker-crm-sync:worker-crm-sync_00: stoppedartisan-schedule:artisan-schedule_00: stoppedworker-es-update:worker-es-update_00: stoppedjiminny-worker-processing-1: jiminny-worker-processing-1_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 jiminny: debugDispatching 100 MatchActivityCrmData jobs (portal=2)Done. Watch logs and run jiminny:debug observeRateLimit to inspect cache state.root@docker_lamp_1:/home/jiminny# php artisan jiminny:debugDispatching 100 MatchActivityCrmData jobs (portal=2)Done. Watch logs and run jiminny:debug observeRateLimit to inspect cache state.root@docker_lamp_1:/home/jiminny# ]ld6]Support Daily - in 1h 17 m100% <78• Mon 11 May 13:43:18T₴1DEV (docker)-zsh84-zshX5screenpipe"2 8665.46ms DONE-zshDEV...
|
17960
|
NULL
|
NULL
|
NULL
|
|
17963
|
783
|
6
|
2026-05-11T10:43:20.781392+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778496200781_m2.jpg...
|
PhpStorm
|
faVsco.js – custom.log
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings...
|
[{"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":"JY-20725-handle-HS-search-rate-limit, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.09541223,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: JY-20725-handle-HS-search-rate-limit","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.82413566,"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":"HandleHubspotRateLimitTest","depth":6,"bounds":{"left":0.8394282,"top":0.019952115,"width":0.076130316,"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 'HandleHubspotRateLimitTest'","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 'HandleHubspotRateLimitTest'","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}]...
|
1579713784452094674
|
-8780890023316608054
|
click
|
hybrid
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
PhostormVIewINavicarecodeLaravelKeractorTOOISWindowFV faVsco.js°9 JY-20725-handle-HS-search-rate-limProiect v© PlaybackController.php(c) HubSpot/Service.onp(C) TranscodeParameterResclT SyncCrmEntitiesTrait.phpC) CachedCrmServiceDecorator.ongC UserService.onp© Uuid.php> D TraitsD UseCases> D User> D Utils› D Validation> OvOphp nelpers.ong© InitialFrontendState.php© Jiminny.phpc) Plan.oho© Serializer.php© TeamScimDetails.phpbootstrap>© build.> contia>@ contrib→ database>O docsfront-end> D lang> node_modules library rootM ohostan>D public> D resourcesv Mroutesphp api.phpphp api_v2.phppnp console.onopnp customer_api.onppnp embedded.ongphp nealtn.onppnp scim.onophp uprotectedweb.phpphp web.phpphp webhook.php>O scriptsv O storage> M debuabar… M frameworkv Mloas.aitianoreê audio. wav= custom.loalT DeleteCrmEntityTrait.php© VerifyActivityCrmTaskJob.php© Job.php© HanBadkequest.pnpC) Kernel.php© PaginationState.phg230242)243[PHONE]24812501256 Gt257263266269270= hubspot-journal-poll.log= laravel log< nhnunit ymus tht is= oauth-nrivate kevTacts naccod. 12 (40 minutes aadlclass Cllent extends Baseclient 1mpLements Hubspotcllentintertacepublic function getPaginatedDataGenerator(&. scotal&: $lastRecordId* Execute a search request against Hubspot CRM objects with rate Limiting.* oparam strina Sobnectrupe The obnect tupe deals'.'contacts', 'calls')@param array<string, mixed> $payload The search payload with filters, sorts, properties, etc* Greturn arrou The searchresconse with "results'.total""nagnnd'keus* Athrows RateLimitExcention When rate Limit is hit* athrows HubsnotExcentionOn APt errorspublic function search(string SobjectType, array Spayload): arraySendnnint = cpf••RASF IIRIreturn $this->executeRequest(function () use ($endpoint, $payload) {Sresponse = $this->getInstance->getClient->request( method: 'POST', Sendpoint, ['json' => $payload]):\Illuminate\Support\Facades\Log::channel( chai'custom_channel')->info('$response' . PHP_EOL . print_r(Sresponse,return: true)):Tlluminate Sunnont Facades Loa. • channel d'custom_channel')->info('$headers' . PHP_EOL . print_r(Sresponse->getHeaders,return: true));return Sresponse->toArrayO:}):* dchrows DealAp1Exceptzon* dchrows Crmexceptionpublic function getupportun1tyßvld(strina Scrmid. array Stields): arravtrySdeal = Sthis-›‹ecuteRequest(fn () => Sthis->qetNewInstance()->crm()->deals()->basicApi@->aetBvId(Sdeal = Sthis->aetNewinstance@->crmO->dealsO->basicAni@->getBvidimolode( separator:" ' Sfields)} catch (NealAniFycention Se) $Sthis->loa-sinfortfHuhsnot Sailed to fetch onnontunitv!.enm idi => ScomTd.Inpason' => Se->ae+Meççane0lA RateLimitException.ong© PaginationConfig.php3 A70 V3 1"suppont Dally • In 1h 1/m100% L2• Mon 11 May 13:43:20HandleHubspotRateLimitTest v« console (PROD]* console (EUl& console (STAGINGIw.19A=laravel.log4 SF [jiminny@localhost]& HS_local [jiminny@localhost][2026-05-07 14:21:15] local.INF0: [Hubspot] DEBUG Getting headers {"Date":["Thu,07 May 2026 14:21:15 GMT"],"Content-Type":["application/json;charset=utf-8"]."Transfer-Encoding": ["chunked"],"conneccion". Keep-alive"n"CF-Ray" : ["9f80deb8db60dc3a-SOF"],"CF-Cache-Status":"DYNAMIC"]"Strict-Transport-Secur1ty":"max-aqe=31536000* 1ncludeSubDomains: preload")accept-encoding"],"access-control-allow-credentials": "false")."server-timing": ["hcid; desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",cfn:desc="9-80de8ercodc3a-TAD""x-content-type-options": ["nosniff"]."x-hubsoot-correlation-id":"019e02d0-6fd8-7812-bdba-885b7ccb3ee3"]07-May-26 14:51:15 GMT; domain=.hubapi.com; Http0nly; Secure; SameSite=None"]."Renont-To".f","endnoints"•!\"url\":\"https:|V/\V/a.nel.cloudflare.com/\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZLzoYdxI%2BIxVpHmsKn30%2BKVA3mFIJ2m7YRECDGS\"group\":\"cf-nel\",\"max_age\":604800}"],"success_fraction\":0.01,"report_to\":\"cf-nel\"\"max_age\":604800}"],"Server": ["cloudflare"]}} {"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab"."trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}W Windsurf Teams 17:45 UTF-8 P 4 spaces ®...
|
17961
|
NULL
|
NULL
|
NULL
|
|
17964
|
782
|
7
|
2026-05-11T10:43:40.869345+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778496220869_m1.jpg...
|
PhpStorm
|
faVsco.js – custom.log
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
3
70
3
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\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 Illuminate\Support\Facades\Redis;
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)
{
$cacheKey = $this->getRateLimitCacheKey();
$cachedRetryAfter = Redis::get($cacheKey);
if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {
throw new RateLimitException(
'Hubspot rate limit (cached circuit-breaker)',
(int) $cachedRetryAfter,
);
}
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'policy' => $this->parsePolicy($e),
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function getRateLimitCacheKey(): string
{
return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());
}
public function isHubspotRateLimit(Throwable $e): bool
{
if ($e instanceof BadRequest
|| $e instanceof DealApiException
|| $e instanceof ContactApiException
|| $e instanceof CompanyApiException
|| $e instanceof \GuzzleHttp\Exception\RequestException
) {
return (int) $e->getCode() === 429;
}
return false;
}
public function parseRetryAfter(Throwable $e): int
{
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$e ' . PHP_EOL . print_r($e, true));
return 5;
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;
}
}
$policy = $this->parsePolicy($e);
if ($policy === 'TEN_SECONDLY_ROLLING') {
return 10;
}
if ($policy === 'SECONDLY') {
return 1;
}
if ($policy === 'DAILY_LIMIT') {
return 600;
}
$this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [
'exception_class' => get_class($e),
]);
return 10;
}
public function parsePolicy(Throwable $e): ?string
{
if (! method_exists($e, 'getResponseBody')) {
return null;
}
$body = $e->getResponseBody();
if (is_string($body)) {
$body = json_decode($body, true) ?? [];
}
if (! is_array($body)) {
return null;
}
$policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;
return is_string($policy) ? strtoupper($policy) : null;
}
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
);
}
/**
* Execute a search request against HubSpot CRM objects with rate limiting.
*
* @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')
* @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.
* @return array The search response with 'results', 'total', 'paging' keys
* @throws RateLimitException When rate limit is hit
* @throws HubspotException On API errors
*/
public function search(string $objectType, array $payload): array
{
$endpoint = self::BASE_URL . "/crm/v3/objects/{$objectType}/search";
return $this->executeRequest(function () use ($endpoint, $payload) {
$response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$response ' . PHP_EOL . print_r($response, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$headers ' . PHP_EOL . print_r($response->getHeaders(), true));
return $response->toArray();
});
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$deal = $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') {
return $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
return $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
}
/**
* @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 (RateLimitException $e) {
// throw $e;
} 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);
}
}
Sync Changes
Hide This Notification
Code changed:...
|
[{"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":"JY-20725-handle-HS-search-rate-limit, menu","depth":5,"on_screen":true,"help_text":"Git Branch: JY-20725-handle-HS-search-rate-limit","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":"HandleHubspotRateLimitTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'HandleHubspotRateLimitTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'HandleHubspotRateLimitTest'","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":"AXStaticText","text":"3","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"70","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"3","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"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\\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 Illuminate\\Support\\Facades\\Redis;\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\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Reacts to a rate limits (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 $cacheKey = $this->getRateLimitCacheKey();\n\n $cachedRetryAfter = Redis::get($cacheKey);\n if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n (int) $cachedRetryAfter,\n );\n }\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);\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 'policy' => $this->parsePolicy($e),\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 getRateLimitCacheKey(): string\n {\n return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());\n }\n\n public function isHubspotRateLimit(Throwable $e): bool\n {\n if ($e instanceof BadRequest\n || $e instanceof DealApiException\n || $e instanceof ContactApiException\n || $e instanceof CompanyApiException\n || $e instanceof \\GuzzleHttp\\Exception\\RequestException\n ) {\n return (int) $e->getCode() === 429;\n }\n\n return false;\n }\n\n public function parseRetryAfter(Throwable $e): int\n {\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$e ' . PHP_EOL . print_r($e, true));\n return 5;\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 $policy = $this->parsePolicy($e);\n if ($policy === 'TEN_SECONDLY_ROLLING') {\n return 10;\n }\n if ($policy === 'SECONDLY') {\n return 1;\n }\n if ($policy === 'DAILY_LIMIT') {\n return 600;\n }\n\n $this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [\n 'exception_class' => get_class($e),\n ]);\n\n return 10;\n }\n\n public function parsePolicy(Throwable $e): ?string\n {\n if (! method_exists($e, 'getResponseBody')) {\n return null;\n }\n\n $body = $e->getResponseBody();\n if (is_string($body)) {\n $body = json_decode($body, true) ?? [];\n }\n\n if (! is_array($body)) {\n return null;\n }\n\n $policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;\n\n return is_string($policy) ? strtoupper($policy) : null;\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 * Execute a search request against HubSpot CRM objects with rate limiting.\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n * @return array The search response with 'results', 'total', 'paging' keys\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n */\n public function search(string $objectType, array $payload): array\n {\n $endpoint = self::BASE_URL . \"/crm/v3/objects/{$objectType}/search\";\n\n return $this->executeRequest(function () use ($endpoint, $payload) {\n $response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$response ' . PHP_EOL . print_r($response, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$headers ' . PHP_EOL . print_r($response->getHeaders(), true));\n\n return $response->toArray();\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 $deal = $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 return $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n return $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\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 (RateLimitException $e) {\n// throw $e;\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\\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 Illuminate\\Support\\Facades\\Redis;\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\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Reacts to a rate limits (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 $cacheKey = $this->getRateLimitCacheKey();\n\n $cachedRetryAfter = Redis::get($cacheKey);\n if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n (int) $cachedRetryAfter,\n );\n }\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);\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 'policy' => $this->parsePolicy($e),\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 getRateLimitCacheKey(): string\n {\n return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());\n }\n\n public function isHubspotRateLimit(Throwable $e): bool\n {\n if ($e instanceof BadRequest\n || $e instanceof DealApiException\n || $e instanceof ContactApiException\n || $e instanceof CompanyApiException\n || $e instanceof \\GuzzleHttp\\Exception\\RequestException\n ) {\n return (int) $e->getCode() === 429;\n }\n\n return false;\n }\n\n public function parseRetryAfter(Throwable $e): int\n {\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$e ' . PHP_EOL . print_r($e, true));\n return 5;\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 $policy = $this->parsePolicy($e);\n if ($policy === 'TEN_SECONDLY_ROLLING') {\n return 10;\n }\n if ($policy === 'SECONDLY') {\n return 1;\n }\n if ($policy === 'DAILY_LIMIT') {\n return 600;\n }\n\n $this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [\n 'exception_class' => get_class($e),\n ]);\n\n return 10;\n }\n\n public function parsePolicy(Throwable $e): ?string\n {\n if (! method_exists($e, 'getResponseBody')) {\n return null;\n }\n\n $body = $e->getResponseBody();\n if (is_string($body)) {\n $body = json_decode($body, true) ?? [];\n }\n\n if (! is_array($body)) {\n return null;\n }\n\n $policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;\n\n return is_string($policy) ? strtoupper($policy) : null;\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 * Execute a search request against HubSpot CRM objects with rate limiting.\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n * @return array The search response with 'results', 'total', 'paging' keys\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n */\n public function search(string $objectType, array $payload): array\n {\n $endpoint = self::BASE_URL . \"/crm/v3/objects/{$objectType}/search\";\n\n return $this->executeRequest(function () use ($endpoint, $payload) {\n $response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$response ' . PHP_EOL . print_r($response, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$headers ' . PHP_EOL . print_r($response->getHeaders(), true));\n\n return $response->toArray();\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 $deal = $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 return $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n return $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\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 (RateLimitException $e) {\n// throw $e;\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":"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}]...
|
3332723435604885151
|
5225697106764630116
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
3
70
3
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\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 Illuminate\Support\Facades\Redis;
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)
{
$cacheKey = $this->getRateLimitCacheKey();
$cachedRetryAfter = Redis::get($cacheKey);
if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {
throw new RateLimitException(
'Hubspot rate limit (cached circuit-breaker)',
(int) $cachedRetryAfter,
);
}
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'policy' => $this->parsePolicy($e),
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function getRateLimitCacheKey(): string
{
return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());
}
public function isHubspotRateLimit(Throwable $e): bool
{
if ($e instanceof BadRequest
|| $e instanceof DealApiException
|| $e instanceof ContactApiException
|| $e instanceof CompanyApiException
|| $e instanceof \GuzzleHttp\Exception\RequestException
) {
return (int) $e->getCode() === 429;
}
return false;
}
public function parseRetryAfter(Throwable $e): int
{
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$e ' . PHP_EOL . print_r($e, true));
return 5;
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;
}
}
$policy = $this->parsePolicy($e);
if ($policy === 'TEN_SECONDLY_ROLLING') {
return 10;
}
if ($policy === 'SECONDLY') {
return 1;
}
if ($policy === 'DAILY_LIMIT') {
return 600;
}
$this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [
'exception_class' => get_class($e),
]);
return 10;
}
public function parsePolicy(Throwable $e): ?string
{
if (! method_exists($e, 'getResponseBody')) {
return null;
}
$body = $e->getResponseBody();
if (is_string($body)) {
$body = json_decode($body, true) ?? [];
}
if (! is_array($body)) {
return null;
}
$policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;
return is_string($policy) ? strtoupper($policy) : null;
}
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
);
}
/**
* Execute a search request against HubSpot CRM objects with rate limiting.
*
* @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')
* @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.
* @return array The search response with 'results', 'total', 'paging' keys
* @throws RateLimitException When rate limit is hit
* @throws HubspotException On API errors
*/
public function search(string $objectType, array $payload): array
{
$endpoint = self::BASE_URL . "/crm/v3/objects/{$objectType}/search";
return $this->executeRequest(function () use ($endpoint, $payload) {
$response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$response ' . PHP_EOL . print_r($response, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$headers ' . PHP_EOL . print_r($response->getHeaders(), true));
return $response->toArray();
});
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$deal = $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') {
return $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
return $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
}
/**
* @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 (RateLimitException $e) {
// throw $e;
} 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);
}
}
Sync Changes
Hide This Notification
Code changed:...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
17965
|
783
|
7
|
2026-05-11T10:43:40.879418+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778496220879_m2.jpg...
|
PhpStorm
|
faVsco.js – custom.log
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
3
70
3
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\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 Illuminate\Support\Facades\Redis;
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)
{
$cacheKey = $this->getRateLimitCacheKey();
$cachedRetryAfter = Redis::get($cacheKey);
if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {
throw new RateLimitException(
'Hubspot rate limit (cached circuit-breaker)',
(int) $cachedRetryAfter,
);
}
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'policy' => $this->parsePolicy($e),
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function getRateLimitCacheKey(): string
{
return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());
}
public function isHubspotRateLimit(Throwable $e): bool
{
if ($e instanceof BadRequest
|| $e instanceof DealApiException
|| $e instanceof ContactApiException
|| $e instanceof CompanyApiException
|| $e instanceof \GuzzleHttp\Exception\RequestException
) {
return (int) $e->getCode() === 429;
}
return false;
}
public function parseRetryAfter(Throwable $e): int
{
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$e ' . PHP_EOL . print_r($e, true));
return 5;
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;
}
}
$policy = $this->parsePolicy($e);
if ($policy === 'TEN_SECONDLY_ROLLING') {
return 10;
}
if ($policy === 'SECONDLY') {
return 1;
}
if ($policy === 'DAILY_LIMIT') {
return 600;
}
$this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [
'exception_class' => get_class($e),
]);
return 10;
}
public function parsePolicy(Throwable $e): ?string
{
if (! method_exists($e, 'getResponseBody')) {
return null;
}
$body = $e->getResponseBody();
if (is_string($body)) {
$body = json_decode($body, true) ?? [];
}
if (! is_array($body)) {
return null;
}
$policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;
return is_string($policy) ? strtoupper($policy) : null;
}
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
);
}
/**
* Execute a search request against HubSpot CRM objects with rate limiting.
*
* @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')
* @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.
* @return array The search response with 'results', 'total', 'paging' keys
* @throws RateLimitException When rate limit is hit
* @throws HubspotException On API errors
*/
public function search(string $objectType, array $payload): array
{
$endpoint = self::BASE_URL . "/crm/v3/objects/{$objectType}/search";
return $this->executeRequest(function () use ($endpoint, $payload) {
$response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$response ' . PHP_EOL . print_r($response, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$headers ' . PHP_EOL . print_r($response->getHeaders(), true));
return $response->toArray();
});
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$deal = $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') {
return $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
return $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
}
/**
* @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 (RateLimitException $e) {
// throw $e;
} 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);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}...
|
[{"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":"JY-20725-handle-HS-search-rate-limit, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.09541223,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: JY-20725-handle-HS-search-rate-limit","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.82413566,"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":"HandleHubspotRateLimitTest","depth":6,"bounds":{"left":0.8394282,"top":0.019952115,"width":0.076130316,"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 'HandleHubspotRateLimitTest'","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 'HandleHubspotRateLimitTest'","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":"AXStaticText","text":"3","depth":4,"bounds":{"left":0.5518617,"top":0.15003991,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"70","depth":4,"bounds":{"left":0.5618351,"top":0.15003991,"width":0.009973404,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"3","depth":4,"bounds":{"left":0.5738032,"top":0.15003991,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.5834442,"top":0.14844373,"width":0.00731383,"height":0.018355945},"on_screen":true,"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.59075797,"top":0.14844373,"width":0.006981383,"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\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\\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 Illuminate\\Support\\Facades\\Redis;\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\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Reacts to a rate limits (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 $cacheKey = $this->getRateLimitCacheKey();\n\n $cachedRetryAfter = Redis::get($cacheKey);\n if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n (int) $cachedRetryAfter,\n );\n }\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);\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 'policy' => $this->parsePolicy($e),\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 getRateLimitCacheKey(): string\n {\n return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());\n }\n\n public function isHubspotRateLimit(Throwable $e): bool\n {\n if ($e instanceof BadRequest\n || $e instanceof DealApiException\n || $e instanceof ContactApiException\n || $e instanceof CompanyApiException\n || $e instanceof \\GuzzleHttp\\Exception\\RequestException\n ) {\n return (int) $e->getCode() === 429;\n }\n\n return false;\n }\n\n public function parseRetryAfter(Throwable $e): int\n {\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$e ' . PHP_EOL . print_r($e, true));\n return 5;\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 $policy = $this->parsePolicy($e);\n if ($policy === 'TEN_SECONDLY_ROLLING') {\n return 10;\n }\n if ($policy === 'SECONDLY') {\n return 1;\n }\n if ($policy === 'DAILY_LIMIT') {\n return 600;\n }\n\n $this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [\n 'exception_class' => get_class($e),\n ]);\n\n return 10;\n }\n\n public function parsePolicy(Throwable $e): ?string\n {\n if (! method_exists($e, 'getResponseBody')) {\n return null;\n }\n\n $body = $e->getResponseBody();\n if (is_string($body)) {\n $body = json_decode($body, true) ?? [];\n }\n\n if (! is_array($body)) {\n return null;\n }\n\n $policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;\n\n return is_string($policy) ? strtoupper($policy) : null;\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 * Execute a search request against HubSpot CRM objects with rate limiting.\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n * @return array The search response with 'results', 'total', 'paging' keys\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n */\n public function search(string $objectType, array $payload): array\n {\n $endpoint = self::BASE_URL . \"/crm/v3/objects/{$objectType}/search\";\n\n return $this->executeRequest(function () use ($endpoint, $payload) {\n $response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$response ' . PHP_EOL . print_r($response, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$headers ' . PHP_EOL . print_r($response->getHeaders(), true));\n\n return $response->toArray();\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 $deal = $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 return $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n return $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\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 (RateLimitException $e) {\n// throw $e;\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\\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 Illuminate\\Support\\Facades\\Redis;\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\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Reacts to a rate limits (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 $cacheKey = $this->getRateLimitCacheKey();\n\n $cachedRetryAfter = Redis::get($cacheKey);\n if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n (int) $cachedRetryAfter,\n );\n }\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);\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 'policy' => $this->parsePolicy($e),\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 getRateLimitCacheKey(): string\n {\n return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());\n }\n\n public function isHubspotRateLimit(Throwable $e): bool\n {\n if ($e instanceof BadRequest\n || $e instanceof DealApiException\n || $e instanceof ContactApiException\n || $e instanceof CompanyApiException\n || $e instanceof \\GuzzleHttp\\Exception\\RequestException\n ) {\n return (int) $e->getCode() === 429;\n }\n\n return false;\n }\n\n public function parseRetryAfter(Throwable $e): int\n {\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$e ' . PHP_EOL . print_r($e, true));\n return 5;\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 $policy = $this->parsePolicy($e);\n if ($policy === 'TEN_SECONDLY_ROLLING') {\n return 10;\n }\n if ($policy === 'SECONDLY') {\n return 1;\n }\n if ($policy === 'DAILY_LIMIT') {\n return 600;\n }\n\n $this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [\n 'exception_class' => get_class($e),\n ]);\n\n return 10;\n }\n\n public function parsePolicy(Throwable $e): ?string\n {\n if (! method_exists($e, 'getResponseBody')) {\n return null;\n }\n\n $body = $e->getResponseBody();\n if (is_string($body)) {\n $body = json_decode($body, true) ?? [];\n }\n\n if (! is_array($body)) {\n return null;\n }\n\n $policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;\n\n return is_string($policy) ? strtoupper($policy) : null;\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 * Execute a search request against HubSpot CRM objects with rate limiting.\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n * @return array The search response with 'results', 'total', 'paging' keys\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n */\n public function search(string $objectType, array $payload): array\n {\n $endpoint = self::BASE_URL . \"/crm/v3/objects/{$objectType}/search\";\n\n return $this->executeRequest(function () use ($endpoint, $payload) {\n $response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$response ' . PHP_EOL . print_r($response, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$headers ' . PHP_EOL . print_r($response->getHeaders(), true));\n\n return $response->toArray();\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 $deal = $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 return $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n return $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\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 (RateLimitException $e) {\n// throw $e;\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":"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":"19","depth":4,"bounds":{"left":0.96276593,"top":0.07581804,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.9740692,"top":0.074221864,"width":0.00731383,"height":0.018355945},"on_screen":true,"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.98138297,"top":0.074221864,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","depth":4,"bounds":{"left":0.61702126,"top":0.0726257,"width":0.38297874,"height":0.9066241},"on_screen":true,"lines":[{"char_start":273,"char_count":32,"bounds":{"left":0.61702126,"top":0.0,"width":0.080119684,"height":0.014365523}},{"char_start":305,"char_count":79,"bounds":{"left":0.61702126,"top":0.0,"width":0.20212767,"height":0.014365523}},{"char_start":384,"char_count":18,"bounds":{"left":0.61702126,"top":0.0,"width":0.043882977,"height":0.014365523}},{"char_start":402,"char_count":21,"bounds":{"left":0.61702126,"top":0.0,"width":0.051861703,"height":0.014365523}},{"char_start":423,"char_count":48,"bounds":{"left":0.61702126,"top":0.0,"width":0.12167553,"height":0.014365523}},{"char_start":471,"char_count":72,"bounds":{"left":0.61702126,"top":0.0015961692,"width":0.18384309,"height":0.014365523}},{"char_start":543,"char_count":40,"bounds":{"left":0.61702126,"top":0.01915403,"width":0.10106383,"height":0.014365523}},{"char_start":583,"char_count":41,"bounds":{"left":0.61702126,"top":0.03671189,"width":0.10372341,"height":0.014365523}},{"char_start":624,"char_count":72,"bounds":{"left":0.61702126,"top":0.054269753,"width":0.18384309,"height":0.014365523}},{"char_start":696,"char_count":219,"bounds":{"left":0.61702126,"top":0.07182761,"width":0.38297874,"height":0.014365523}},{"char_start":915,"char_count":83,"bounds":{"left":0.61702126,"top":0.08938547,"width":0.21243352,"height":0.014365523}},{"char_start":998,"char_count":20,"bounds":{"left":0.61702126,"top":0.10694334,"width":0.04920213,"height":0.014365523}},{"char_start":1018,"char_count":17,"bounds":{"left":0.61702126,"top":0.1245012,"width":0.041223403,"height":0.014365523}},{"char_start":1035,"char_count":203,"bounds":{"left":0.61702126,"top":0.14205906,"width":0.38297874,"height":0.014365523}},{"char_start":1238,"char_count":22,"bounds":{"left":0.61702126,"top":0.15961692,"width":0.05418883,"height":0.014365523}},{"char_start":1260,"char_count":23,"bounds":{"left":0.61702126,"top":0.17717478,"width":0.056848403,"height":0.014365523}},{"char_start":1283,"char_count":10,"bounds":{"left":0.61702126,"top":0.19473264,"width":0.023271276,"height":0.014365523}},{"char_start":1293,"char_count":27,"bounds":{"left":0.61702126,"top":0.2122905,"width":0.06715426,"height":0.014365523}},{"char_start":1320,"char_count":26,"bounds":{"left":0.61702126,"top":0.22984837,"width":0.06482713,"height":0.014365523}},{"char_start":1346,"char_count":23,"bounds":{"left":0.61702126,"top":0.24740623,"width":0.056848403,"height":0.014365523}},{"char_start":1369,"char_count":28,"bounds":{"left":0.61702126,"top":0.26496407,"width":0.06981383,"height":0.014365523}},{"char_start":1397,"char_count":57,"bounds":{"left":0.61702126,"top":0.28252193,"width":0.14494681,"height":0.014365523}}],"value":"[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {\n\"headers\":{\n\"Date\":[\"Thu,07 May 2026 14:21:15 GMT\"],\n \"Content-Type\":[\"application/json;charset=utf-8\"],\n \"Transfer-Encoding\":[\"chunked\"],\n \"Connection\":[\"keep-alive\"],\n \"CF-Ray\":[\"9f80deb8db60dc3a-SOF\"],\n \"CF-Cache-Status\":[\"DYNAMIC\"],\n \"Strict-Transport-Security\":[\"max-age=31536000; includeSubDomains; preload\"],\n \"Vary\":[\"origin,\n accept-encoding\"],\n \"access-control-allow-credentials\":[\"false\"],\n \"server-timing\":[\"hcid;desc=\\\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\\\",\n cfr;desc=\\\"9f80deb8e7c6dc3a-IAD\\\"\"],\n \"x-content-type-options\":[\"nosniff\"],\n \"x-hubspot-correlation-id\":[\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\"],\n \"Set-Cookie\":[\"__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-1.0.1.1-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,\n 07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None\"],\n \"Report-To\":[\"{\n\\\"endpoints\\\":[{\n\\\"url\\\":\\\"https:\\\\/\\\\/a.nel.cloudflare.com\\\\/report\\\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\\\"}],\n\\\"group\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"NEL\":[\"{\n\\\"success_fraction\\\":0.01,\n\\\"report_to\\\":\\\"cf-nel\\\",\n\\\"max_age\\\":604800}\"],\n\"Server\":[\"cloudflare\"]}} {\n\"correlation_id\":\"95236535-ec98-4541-b92a-adfa73b69eab\",\n\"trace_id\":\"c7ab8365-903f-46d4-9403-0e5b551e3545\"}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
3913560446582010287
|
6378618611371477092
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
3
70
3
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\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 Illuminate\Support\Facades\Redis;
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)
{
$cacheKey = $this->getRateLimitCacheKey();
$cachedRetryAfter = Redis::get($cacheKey);
if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {
throw new RateLimitException(
'Hubspot rate limit (cached circuit-breaker)',
(int) $cachedRetryAfter,
);
}
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'policy' => $this->parsePolicy($e),
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function getRateLimitCacheKey(): string
{
return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());
}
public function isHubspotRateLimit(Throwable $e): bool
{
if ($e instanceof BadRequest
|| $e instanceof DealApiException
|| $e instanceof ContactApiException
|| $e instanceof CompanyApiException
|| $e instanceof \GuzzleHttp\Exception\RequestException
) {
return (int) $e->getCode() === 429;
}
return false;
}
public function parseRetryAfter(Throwable $e): int
{
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$e ' . PHP_EOL . print_r($e, true));
return 5;
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;
}
}
$policy = $this->parsePolicy($e);
if ($policy === 'TEN_SECONDLY_ROLLING') {
return 10;
}
if ($policy === 'SECONDLY') {
return 1;
}
if ($policy === 'DAILY_LIMIT') {
return 600;
}
$this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [
'exception_class' => get_class($e),
]);
return 10;
}
public function parsePolicy(Throwable $e): ?string
{
if (! method_exists($e, 'getResponseBody')) {
return null;
}
$body = $e->getResponseBody();
if (is_string($body)) {
$body = json_decode($body, true) ?? [];
}
if (! is_array($body)) {
return null;
}
$policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;
return is_string($policy) ? strtoupper($policy) : null;
}
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
);
}
/**
* Execute a search request against HubSpot CRM objects with rate limiting.
*
* @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')
* @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.
* @return array The search response with 'results', 'total', 'paging' keys
* @throws RateLimitException When rate limit is hit
* @throws HubspotException On API errors
*/
public function search(string $objectType, array $payload): array
{
$endpoint = self::BASE_URL . "/crm/v3/objects/{$objectType}/search";
return $this->executeRequest(function () use ($endpoint, $payload) {
$response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$response ' . PHP_EOL . print_r($response, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$headers ' . PHP_EOL . print_r($response->getHeaders(), true));
return $response->toArray();
});
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$deal = $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') {
return $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
return $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
}
/**
* @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 (RateLimitException $e) {
// throw $e;
} 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);
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
19
Previous Highlighted Error
Next Highlighted Error
[2026-05-07 14:21:15] local.INFO: [Hubspot] DEBUG Getting headers {
"headers":{
"Date":["Thu,07 May 2026 14:21:15 GMT"],
"Content-Type":["application/json;charset=utf-8"],
"Transfer-Encoding":["chunked"],
"Connection":["keep-alive"],
"CF-Ray":["9f80deb8db60dc3a-SOF"],
"CF-Cache-Status":["DYNAMIC"],
"Strict-Transport-Security":["max-age=31536000; includeSubDomains; preload"],
"Vary":["origin,
accept-encoding"],
"access-control-allow-credentials":["false"],
"server-timing":["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",
cfr;desc=\"9f80deb8e7c6dc3a-IAD\""],
"x-content-type-options":["nosniff"],
"x-hubspot-correlation-id":["019e02d0-6fd8-7812-bdba-885b7ccb3ee3"],
"Set-Cookie":["__cf_bm=SIUrtdQgXVrik50pdqF6hZVYKhzTnQBidvMabeCtm0Y-1778163675-[IP_ADDRESS]-rI.ZggtDKxTge5zr8_2gbBfWMQQ.ufZEXDZyHz2mBUFdzdo2gTHEsOkXMSEShjK0hGYxNhUGM1ZoBpX7BcFZcHEjA7Cs_.SMUhUnd2nYjko; path=/; expires=Thu,
07-May-26 14:51:15 GMT; domain=.hubapi.com; HttpOnly; Secure; SameSite=None"],
"Report-To":["{
\"endpoints\":[{
\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZlzoYdxI%2BIxVpHmsKn3O%2BKVA3mFIJ2m7YRECDGSM%2BW2IYTzo6FM4%2BdUIjURO8srzKSvJgZ%2BQ6R79arKQw3uHLlX\"}],
\"group\":\"cf-nel\",
\"max_age\":604800}"],
"NEL":["{
\"success_fraction\":0.01,
\"report_to\":\"cf-nel\",
\"max_age\":604800}"],
"Server":["cloudflare"]}} {
"correlation_id":"95236535-ec98-4541-b92a-adfa73b69eab",
"trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
17966
|
782
|
8
|
2026-05-11T10:43:44.126670+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778496224126_m1.jpg...
|
PhpStorm
|
faVsco.js – laravel.log
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
iTerm2ShellEditViewSessionScriptsProfilesWindowHel iTerm2ShellEditViewSessionScriptsProfilesWindowHelpld6]Support Daily - in 1h 17 m100% <8• Mon 11 May 13:43:43T81DEV (docker)-zshDOCKER₴81DEV (docker)$2APP (-zsh)H3viewsjiminny-worker-processing-2:jiminny-worker-processing-2_00: stoppedjiminny-worker-processing-3:jiminny-worker-processing-3_00: stoppedjiminny-worker-processing-4:jiminny-worker-processing-4_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-conferences:worker-conferences_00: stoppedworker-crm-update:worker-crm-update_00: stoppedworker-download:worker-download_00: stoppedworker-emails:worker-emails_00: stoppedworker-nudges:worker-nudges_00: stoppedworker:worker_00: stoppedworker-audio:worker-audio_00: stoppedworker-calendar:worker-calendar_00: stoppedworker-crm-sync:worker-crm-sync_00: stoppedartisan-schedule:artisan-schedule_00: stoppedworker-es-update:worker-es-update_00: stoppedjiminny-worker-processing-1: jiminny-worker-processing-1_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 jiminny: debugDispatching 100 MatchActivityCrmData jobs (portal=2)Done. Watch logs and run jiminny:debug observeRateLimit to inspect cache state.root@docker_lamp_1:/home/jiminny# php artisan jiminny:debugDispatching 100 MatchActivityCrmData jobs (portal=2)Done. Watch logs and run jiminny:debug observeRateLimit to inspect cache state.root@docker_lamp_1:/home/jiminny# ]84-zshX5screenpipe"O 8865.46ms DONE-zshDEV...
|
NULL
|
9084699997369329956
|
NULL
|
click
|
ocr
|
NULL
|
iTerm2ShellEditViewSessionScriptsProfilesWindowHel iTerm2ShellEditViewSessionScriptsProfilesWindowHelpld6]Support Daily - in 1h 17 m100% <8• Mon 11 May 13:43:43T81DEV (docker)-zshDOCKER₴81DEV (docker)$2APP (-zsh)H3viewsjiminny-worker-processing-2:jiminny-worker-processing-2_00: stoppedjiminny-worker-processing-3:jiminny-worker-processing-3_00: stoppedjiminny-worker-processing-4:jiminny-worker-processing-4_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-conferences:worker-conferences_00: stoppedworker-crm-update:worker-crm-update_00: stoppedworker-download:worker-download_00: stoppedworker-emails:worker-emails_00: stoppedworker-nudges:worker-nudges_00: stoppedworker:worker_00: stoppedworker-audio:worker-audio_00: stoppedworker-calendar:worker-calendar_00: stoppedworker-crm-sync:worker-crm-sync_00: stoppedartisan-schedule:artisan-schedule_00: stoppedworker-es-update:worker-es-update_00: stoppedjiminny-worker-processing-1: jiminny-worker-processing-1_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 jiminny: debugDispatching 100 MatchActivityCrmData jobs (portal=2)Done. Watch logs and run jiminny:debug observeRateLimit to inspect cache state.root@docker_lamp_1:/home/jiminny# php artisan jiminny:debugDispatching 100 MatchActivityCrmData jobs (portal=2)Done. Watch logs and run jiminny:debug observeRateLimit to inspect cache state.root@docker_lamp_1:/home/jiminny# ]84-zshX5screenpipe"O 8865.46ms DONE-zshDEV...
|
17964
|
NULL
|
NULL
|
NULL
|
|
17967
|
783
|
8
|
2026-05-11T10:43:44.126688+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778496224126_m2.jpg...
|
PhpStorm
|
faVsco.js – laravel.log
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
PhostormVIewINavicareCodeFV faVsco.js°9 JY-20725-h PhostormVIewINavicareCodeFV faVsco.js°9 JY-20725-handle-HS-search-rate-IiyProiect© BatchSyncCollectole balchsynckealsseo closedDealstagessDealrielasservice.gc)Decorateacuivilv.or© FieldDefinitions.phrC) FieldT vpeconvertee Hubspotclientinterlc) Hubspotlokenman© PayloadBuilder.phpC) RemotecrmobiectnP ResponseNormalize256 Gc) Service.onrC) SvncFieldAction.ohC) SvncRelatedActivit 260C) WebhookSvncBatclv MintearationAorM AcceccorsConfigDDTO264• M SiltersD Jobs• M ProcnectSearchStr.W sevice lraits© DataClient.php© DecorateActivity.ph(e)LocalSearch.onp© LocalSearchInterfac© RemoteSearch.php275c) Service.php27€|/1v D Listeners© ConvertLeadActivit278c) PurceLookuocache> M Metadata> MiarationiM Pipedrivev Salesforce• D FieldsM OnnortunitvMatcheMOnnortunitvSvneStM ProsneetSearchStr.> M Service TraiteC) Client nhr@ DecorateActivity.ph. Delete@biectsTrait.@ FieldDefinitions.php© PayloadBuilder.php© Profile.php© QueryBuilder.phpTacts naccod. 12 (41 minutes addC) HubSpot/Service.phpoveryaeuvitycrmlaskJoo.pnp© Job.phpBadkequest.phpclass Cllent extends Baseclient 1mpLements Hubspotcllentintertace* doaram scrino sobecclupe Ine obvect cupe deals',"companzes", "contacts", "calls)* @param array<string, mixed› $payload The search payload with filters, sorts, properties, etc.* dretun arrau The search response with 'results'."total", "paqing' keus* Athrows RateLimitException When rate limit is hit* athrows HubspotException On APIpubuic function search(strina Sobnectlvoe, array Spavload): arravSendoonnt = self::BASE URL Areturn Sthis->executeReauestfunction O use Sendooint. Spavload) <Sresponse = Sthis->aetinstance@->netcliento->reauestcmod: 'POST', Sendpoint, ['json' => $payload]):illuminate Suoport Facades Loa: :channelo chan'custom channel")->info('Sresponse 1 , PHP EOL )orint rSresponse. return: true))lnhuminate Sunnont Facades "na. channeic'custom_channel')->info('$headers' . PHP_EOL • print_r(Sresponse->getHeaders,return: +rue))*neturn Snesnonsp->toAnnavd•)PaginationConrig.pnp* acmows veaLApzexcepclon* Othrows CrmExceptionoubulc tunccion cecupoorcunltybyla scrino scrmla, array stlelas. arraytry{$deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(Sdeal = sth1s->qetNewinstance0->crmo->deals->bas1cAp10->qetByldescrmid1molode separator. ".", Stlelds)} catch (DealApiException $e) {Sthis->lo0->info(' Hubsoot Failed to fetch ooportunity'. ""reason' => Se->aetMessadeOuthrow Se.i4 (1 Sdeal instancenf NealWithAscociations) ₫+hnow now (omSycontionl meccaad'Deal not found'):notunn""suppont Dally • In 1h 1/m100% Lz• Mon 11 May 13:43:43HandleHubspotRateLimitTest v« console (PROD]* console (EUl& console (STAGINGIw.19A= laravel.log4 SF [jiminny@localhost]& HS_local [jiminny@localhost][2026-05-07 14:21:15] local.INF0: [Hubspot] DEBUG Getting headers {"Date":["Thu,07 May 2026 14:21:15 GMT"],"Content-Type":["application/json;charset=utf-8"]."Transfer-Encoding": ["chunked"],"conneccion". Keep-alive"n"CF-Ray": ["9f80deb8db60dc3a-SOF"],"CF-Cache-Status":"DYNAMIC"]"Strict-Transport-Secur1ty":"max-aqe=31536000* 1ncludeSubDomains: preload")accept-encoding"],"access-control-allow-credentials": "false").server-timing": ["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",cfn:desc="9-80de8ercodcSa-TAD"'"x-content-type-options": ["nosniff"]."x-hubsoot-correlation-id":"019e02d0-6fd8-7812-bdba-885b7ccb3ee3"]07-May-26 14:51:15 GMT; domain=.hubapi.com; Http0nly; Secure; SameSite=None"]."Report-To":["1"endnoints"•!\"url\":\"https:|V/\V/a.nel.cloudflare.com/\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZLzoYdxI%2BIxVpHmsKn30%2BKVA3mFIJ2m7YRECDGS\"group\":\"cf-nel\",\"max_age\":604800}"],"success_fraction\":0.01,"report_to\":\"cf-nel\"\"max_age\":604800}"],"Server": ["cloudflare"]}} {"correlation id":"95236535-ec98-4541-b92a-adfa73b69eab"."trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}W Windsurf Teams 266:12 UTF-8 4 spaces ®...
|
NULL
|
-3046095407443504155
|
NULL
|
click
|
ocr
|
NULL
|
PhostormVIewINavicareCodeFV faVsco.js°9 JY-20725-h PhostormVIewINavicareCodeFV faVsco.js°9 JY-20725-handle-HS-search-rate-IiyProiect© BatchSyncCollectole balchsynckealsseo closedDealstagessDealrielasservice.gc)Decorateacuivilv.or© FieldDefinitions.phrC) FieldT vpeconvertee Hubspotclientinterlc) Hubspotlokenman© PayloadBuilder.phpC) RemotecrmobiectnP ResponseNormalize256 Gc) Service.onrC) SvncFieldAction.ohC) SvncRelatedActivit 260C) WebhookSvncBatclv MintearationAorM AcceccorsConfigDDTO264• M SiltersD Jobs• M ProcnectSearchStr.W sevice lraits© DataClient.php© DecorateActivity.ph(e)LocalSearch.onp© LocalSearchInterfac© RemoteSearch.php275c) Service.php27€|/1v D Listeners© ConvertLeadActivit278c) PurceLookuocache> M Metadata> MiarationiM Pipedrivev Salesforce• D FieldsM OnnortunitvMatcheMOnnortunitvSvneStM ProsneetSearchStr.> M Service TraiteC) Client nhr@ DecorateActivity.ph. Delete@biectsTrait.@ FieldDefinitions.php© PayloadBuilder.php© Profile.php© QueryBuilder.phpTacts naccod. 12 (41 minutes addC) HubSpot/Service.phpoveryaeuvitycrmlaskJoo.pnp© Job.phpBadkequest.phpclass Cllent extends Baseclient 1mpLements Hubspotcllentintertace* doaram scrino sobecclupe Ine obvect cupe deals',"companzes", "contacts", "calls)* @param array<string, mixed› $payload The search payload with filters, sorts, properties, etc.* dretun arrau The search response with 'results'."total", "paqing' keus* Athrows RateLimitException When rate limit is hit* athrows HubspotException On APIpubuic function search(strina Sobnectlvoe, array Spavload): arravSendoonnt = self::BASE URL Areturn Sthis->executeReauestfunction O use Sendooint. Spavload) <Sresponse = Sthis->aetinstance@->netcliento->reauestcmod: 'POST', Sendpoint, ['json' => $payload]):illuminate Suoport Facades Loa: :channelo chan'custom channel")->info('Sresponse 1 , PHP EOL )orint rSresponse. return: true))lnhuminate Sunnont Facades "na. channeic'custom_channel')->info('$headers' . PHP_EOL • print_r(Sresponse->getHeaders,return: +rue))*neturn Snesnonsp->toAnnavd•)PaginationConrig.pnp* acmows veaLApzexcepclon* Othrows CrmExceptionoubulc tunccion cecupoorcunltybyla scrino scrmla, array stlelas. arraytry{$deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(Sdeal = sth1s->qetNewinstance0->crmo->deals->bas1cAp10->qetByldescrmid1molode separator. ".", Stlelds)} catch (DealApiException $e) {Sthis->lo0->info(' Hubsoot Failed to fetch ooportunity'. ""reason' => Se->aetMessadeOuthrow Se.i4 (1 Sdeal instancenf NealWithAscociations) ₫+hnow now (omSycontionl meccaad'Deal not found'):notunn""suppont Dally • In 1h 1/m100% Lz• Mon 11 May 13:43:43HandleHubspotRateLimitTest v« console (PROD]* console (EUl& console (STAGINGIw.19A= laravel.log4 SF [jiminny@localhost]& HS_local [jiminny@localhost][2026-05-07 14:21:15] local.INF0: [Hubspot] DEBUG Getting headers {"Date":["Thu,07 May 2026 14:21:15 GMT"],"Content-Type":["application/json;charset=utf-8"]."Transfer-Encoding": ["chunked"],"conneccion". Keep-alive"n"CF-Ray": ["9f80deb8db60dc3a-SOF"],"CF-Cache-Status":"DYNAMIC"]"Strict-Transport-Secur1ty":"max-aqe=31536000* 1ncludeSubDomains: preload")accept-encoding"],"access-control-allow-credentials": "false").server-timing": ["hcid;desc=\"019e02d0-6fd8-7812-bdba-885b7ccb3ee3\",cfn:desc="9-80de8ercodcSa-TAD"'"x-content-type-options": ["nosniff"]."x-hubsoot-correlation-id":"019e02d0-6fd8-7812-bdba-885b7ccb3ee3"]07-May-26 14:51:15 GMT; domain=.hubapi.com; Http0nly; Secure; SameSite=None"]."Report-To":["1"endnoints"•!\"url\":\"https:|V/\V/a.nel.cloudflare.com/\/report\\/v4?s=NYAlsVTP0fYm32qrSDjxYE4sd2RWRqiSp3wHsmdEgZLzoYdxI%2BIxVpHmsKn30%2BKVA3mFIJ2m7YRECDGS\"group\":\"cf-nel\",\"max_age\":604800}"],"success_fraction\":0.01,"report_to\":\"cf-nel\"\"max_age\":604800}"],"Server": ["cloudflare"]}} {"correlation id":"95236535-ec98-4541-b92a-adfa73b69eab"."trace_id":"c7ab8365-903f-46d4-9403-0e5b551e3545"}W Windsurf Teams 266:12 UTF-8 4 spaces ®...
|
17965
|
NULL
|
NULL
|
NULL
|
|
17968
|
782
|
9
|
2026-05-11T10:43:47.802658+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778496227802_m1.jpg...
|
PhpStorm
|
faVsco.js – laravel.log
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
3
70
3
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\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 Illuminate\Support\Facades\Redis;
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)
{
$cacheKey = $this->getRateLimitCacheKey();
$cachedRetryAfter = Redis::get($cacheKey);
if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {
throw new RateLimitException(
'Hubspot rate limit (cached circuit-breaker)',
(int) $cachedRetryAfter,
);
}
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'policy' => $this->parsePolicy($e),
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function getRateLimitCacheKey(): string
{
return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());
}
public function isHubspotRateLimit(Throwable $e): bool
{
if ($e instanceof BadRequest
|| $e instanceof DealApiException
|| $e instanceof ContactApiException
|| $e instanceof CompanyApiException
|| $e instanceof \GuzzleHttp\Exception\RequestException
) {
return (int) $e->getCode() === 429;
}
return false;
}
public function parseRetryAfter(Throwable $e): int
{
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$e ' . PHP_EOL . print_r($e, true));
return 5;
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;
}
}
$policy = $this->parsePolicy($e);
if ($policy === 'TEN_SECONDLY_ROLLING') {
return 10;
}
if ($policy === 'SECONDLY') {
return 1;
}
if ($policy === 'DAILY_LIMIT') {
return 600;
}
$this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [
'exception_class' => get_class($e),
]);
return 10;
}
public function parsePolicy(Throwable $e): ?string
{
if (! method_exists($e, 'getResponseBody')) {
return null;
}
$body = $e->getResponseBody();
if (is_string($body)) {
$body = json_decode($body, true) ?? [];
}
if (! is_array($body)) {
return null;
}
$policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;
return is_string($policy) ? strtoupper($policy) : null;
}
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
);
}
/**
* Execute a search request against HubSpot CRM objects with rate limiting.
*
* @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')
* @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.
* @return array The search response with 'results', 'total', 'paging' keys
* @throws RateLimitException When rate limit is hit
* @throws HubspotException On API errors
*/
public function search(string $objectType, array $payload): array
{
$endpoint = self::BASE_URL . "/crm/v3/objects/{$objectType}/search";
return $this->executeRequest(function () use ($endpoint, $payload) {
$response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$response ' . PHP_EOL . print_r($response, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$headers ' . PHP_EOL . print_r($response->getHeaders(), true));
return $response->toArray();
});
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$deal = $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') {
return $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
return $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
}
/**
* @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 (RateLimitException $e) {
// throw $e;
} 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);
}
}
Show Replace Field
Search History
Received 429 from API
New Line
Match Case...
|
[{"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":"JY-20725-handle-HS-search-rate-limit, menu","depth":5,"on_screen":true,"help_text":"Git Branch: JY-20725-handle-HS-search-rate-limit","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":"HandleHubspotRateLimitTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'HandleHubspotRateLimitTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'HandleHubspotRateLimitTest'","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":"AXStaticText","text":"3","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"70","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"3","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"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\\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 Illuminate\\Support\\Facades\\Redis;\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\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Reacts to a rate limits (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 $cacheKey = $this->getRateLimitCacheKey();\n\n $cachedRetryAfter = Redis::get($cacheKey);\n if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n (int) $cachedRetryAfter,\n );\n }\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);\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 'policy' => $this->parsePolicy($e),\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 getRateLimitCacheKey(): string\n {\n return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());\n }\n\n public function isHubspotRateLimit(Throwable $e): bool\n {\n if ($e instanceof BadRequest\n || $e instanceof DealApiException\n || $e instanceof ContactApiException\n || $e instanceof CompanyApiException\n || $e instanceof \\GuzzleHttp\\Exception\\RequestException\n ) {\n return (int) $e->getCode() === 429;\n }\n\n return false;\n }\n\n public function parseRetryAfter(Throwable $e): int\n {\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$e ' . PHP_EOL . print_r($e, true));\n return 5;\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 $policy = $this->parsePolicy($e);\n if ($policy === 'TEN_SECONDLY_ROLLING') {\n return 10;\n }\n if ($policy === 'SECONDLY') {\n return 1;\n }\n if ($policy === 'DAILY_LIMIT') {\n return 600;\n }\n\n $this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [\n 'exception_class' => get_class($e),\n ]);\n\n return 10;\n }\n\n public function parsePolicy(Throwable $e): ?string\n {\n if (! method_exists($e, 'getResponseBody')) {\n return null;\n }\n\n $body = $e->getResponseBody();\n if (is_string($body)) {\n $body = json_decode($body, true) ?? [];\n }\n\n if (! is_array($body)) {\n return null;\n }\n\n $policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;\n\n return is_string($policy) ? strtoupper($policy) : null;\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 * Execute a search request against HubSpot CRM objects with rate limiting.\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n * @return array The search response with 'results', 'total', 'paging' keys\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n */\n public function search(string $objectType, array $payload): array\n {\n $endpoint = self::BASE_URL . \"/crm/v3/objects/{$objectType}/search\";\n\n return $this->executeRequest(function () use ($endpoint, $payload) {\n $response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$response ' . PHP_EOL . print_r($response, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$headers ' . PHP_EOL . print_r($response->getHeaders(), true));\n\n return $response->toArray();\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 $deal = $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 return $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n return $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\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 (RateLimitException $e) {\n// throw $e;\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\\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 Illuminate\\Support\\Facades\\Redis;\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\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Reacts to a rate limits (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 $cacheKey = $this->getRateLimitCacheKey();\n\n $cachedRetryAfter = Redis::get($cacheKey);\n if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n (int) $cachedRetryAfter,\n );\n }\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);\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 'policy' => $this->parsePolicy($e),\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 getRateLimitCacheKey(): string\n {\n return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());\n }\n\n public function isHubspotRateLimit(Throwable $e): bool\n {\n if ($e instanceof BadRequest\n || $e instanceof DealApiException\n || $e instanceof ContactApiException\n || $e instanceof CompanyApiException\n || $e instanceof \\GuzzleHttp\\Exception\\RequestException\n ) {\n return (int) $e->getCode() === 429;\n }\n\n return false;\n }\n\n public function parseRetryAfter(Throwable $e): int\n {\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$e ' . PHP_EOL . print_r($e, true));\n return 5;\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 $policy = $this->parsePolicy($e);\n if ($policy === 'TEN_SECONDLY_ROLLING') {\n return 10;\n }\n if ($policy === 'SECONDLY') {\n return 1;\n }\n if ($policy === 'DAILY_LIMIT') {\n return 600;\n }\n\n $this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [\n 'exception_class' => get_class($e),\n ]);\n\n return 10;\n }\n\n public function parsePolicy(Throwable $e): ?string\n {\n if (! method_exists($e, 'getResponseBody')) {\n return null;\n }\n\n $body = $e->getResponseBody();\n if (is_string($body)) {\n $body = json_decode($body, true) ?? [];\n }\n\n if (! is_array($body)) {\n return null;\n }\n\n $policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;\n\n return is_string($policy) ? strtoupper($policy) : null;\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 * Execute a search request against HubSpot CRM objects with rate limiting.\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n * @return array The search response with 'results', 'total', 'paging' keys\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n */\n public function search(string $objectType, array $payload): array\n {\n $endpoint = self::BASE_URL . \"/crm/v3/objects/{$objectType}/search\";\n\n return $this->executeRequest(function () use ($endpoint, $payload) {\n $response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$response ' . PHP_EOL . print_r($response, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$headers ' . PHP_EOL . print_r($response->getHeaders(), true));\n\n return $response->toArray();\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 $deal = $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 return $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n return $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\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 (RateLimitException $e) {\n// throw $e;\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":"AXButton","text":"Show Replace Field","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Search History","depth":3,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"Received 429 from API","depth":4,"on_screen":true,"value":"Received 429 from API","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"New Line","depth":3,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Match Case","depth":3,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
5032120036760849936
|
5225697106764630116
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
3
70
3
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\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 Illuminate\Support\Facades\Redis;
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)
{
$cacheKey = $this->getRateLimitCacheKey();
$cachedRetryAfter = Redis::get($cacheKey);
if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {
throw new RateLimitException(
'Hubspot rate limit (cached circuit-breaker)',
(int) $cachedRetryAfter,
);
}
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'policy' => $this->parsePolicy($e),
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function getRateLimitCacheKey(): string
{
return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());
}
public function isHubspotRateLimit(Throwable $e): bool
{
if ($e instanceof BadRequest
|| $e instanceof DealApiException
|| $e instanceof ContactApiException
|| $e instanceof CompanyApiException
|| $e instanceof \GuzzleHttp\Exception\RequestException
) {
return (int) $e->getCode() === 429;
}
return false;
}
public function parseRetryAfter(Throwable $e): int
{
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$e ' . PHP_EOL . print_r($e, true));
return 5;
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;
}
}
$policy = $this->parsePolicy($e);
if ($policy === 'TEN_SECONDLY_ROLLING') {
return 10;
}
if ($policy === 'SECONDLY') {
return 1;
}
if ($policy === 'DAILY_LIMIT') {
return 600;
}
$this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [
'exception_class' => get_class($e),
]);
return 10;
}
public function parsePolicy(Throwable $e): ?string
{
if (! method_exists($e, 'getResponseBody')) {
return null;
}
$body = $e->getResponseBody();
if (is_string($body)) {
$body = json_decode($body, true) ?? [];
}
if (! is_array($body)) {
return null;
}
$policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;
return is_string($policy) ? strtoupper($policy) : null;
}
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
);
}
/**
* Execute a search request against HubSpot CRM objects with rate limiting.
*
* @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')
* @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.
* @return array The search response with 'results', 'total', 'paging' keys
* @throws RateLimitException When rate limit is hit
* @throws HubspotException On API errors
*/
public function search(string $objectType, array $payload): array
{
$endpoint = self::BASE_URL . "/crm/v3/objects/{$objectType}/search";
return $this->executeRequest(function () use ($endpoint, $payload) {
$response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$response ' . PHP_EOL . print_r($response, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$headers ' . PHP_EOL . print_r($response->getHeaders(), true));
return $response->toArray();
});
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$deal = $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') {
return $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
return $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
}
/**
* @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 (RateLimitException $e) {
// throw $e;
} 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);
}
}
Show Replace Field
Search History
Received 429 from API
New Line
Match Case...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
17969
|
783
|
9
|
2026-05-11T10:43:48.281288+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778496228281_m2.jpg...
|
PhpStorm
|
faVsco.js – laravel.log
|
1
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
3
70
3
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\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 Illuminate\Support\Facades\Redis;
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)
{
$cacheKey = $this->getRateLimitCacheKey();
$cachedRetryAfter = Redis::get($cacheKey);
if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {
throw new RateLimitException(
'Hubspot rate limit (cached circuit-breaker)',
(int) $cachedRetryAfter,
);
}
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'policy' => $this->parsePolicy($e),
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function getRateLimitCacheKey(): string
{
return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());
}
public function isHubspotRateLimit(Throwable $e): bool
{
if ($e instanceof BadRequest
|| $e instanceof DealApiException
|| $e instanceof ContactApiException
|| $e instanceof CompanyApiException
|| $e instanceof \GuzzleHttp\Exception\RequestException
) {
return (int) $e->getCode() === 429;
}
return false;
}
public function parseRetryAfter(Throwable $e): int
{
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$e ' . PHP_EOL . print_r($e, true));
return 5;
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;
}
}
$policy = $this->parsePolicy($e);
if ($policy === 'TEN_SECONDLY_ROLLING') {
return 10;
}
if ($policy === 'SECONDLY') {
return 1;
}
if ($policy === 'DAILY_LIMIT') {
return 600;
}
$this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [
'exception_class' => get_class($e),
]);
return 10;
}
public function parsePolicy(Throwable $e): ?string
{
if (! method_exists($e, 'getResponseBody')) {
return null;
}
$body = $e->getResponseBody();
if (is_string($body)) {
$body = json_decode($body, true) ?? [];
}
if (! is_array($body)) {
return null;
}
$policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;
return is_string($policy) ? strtoupper($policy) : null;
}
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
);
}
/**
* Execute a search request against HubSpot CRM objects with rate limiting.
*
* @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')
* @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.
* @return array The search response with 'results', 'total', 'paging' keys
* @throws RateLimitException When rate limit is hit
* @throws HubspotException On API errors
*/
public function search(string $objectType, array $payload): array
{
$endpoint = self::BASE_URL . "/crm/v3/objects/{$objectType}/search";
return $this->executeRequest(function () use ($endpoint, $payload) {
$response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$response ' . PHP_EOL . print_r($response, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$headers ' . PHP_EOL . print_r($response->getHeaders(), true));
return $response->toArray();
});
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$deal = $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') {
return $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
return $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
}
/**
* @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 (RateLimitException $e) {
// throw $e;
} 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);
}
}
Show Replace Field
Search History
Received 429 from API
New Line
Match Case
Words
Regex
Replace History
Replace
New Line
Preserve case
0 results
Previous Occurrence
Next Occurrence
Filter Search Results
Open in Window, Multiple Cursors
Click to highlight
Close
Sync Changes
Hide This Notification
Code changed:
Hide
[2026-05-11 10:41:16] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"mailbox:batch:process","memoryBeforeCommandInMb":60.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"ee9b9096-b1f1-4e2b-ac17-af0003eb00c0","trace_id":"8904c61b-d5b4-44cc-91bf-68f0f73222c0"}
[2026-05-11 10:41:16] local.INFO: [EmailSchedule] STARTING batch process {"host":"docker_lamp_1"} {"correlation_id":"ee9b9096-b1f1-4e2b-ac17-af0003eb00c0","trace_id":"8904c61b-d5b4-44cc-91bf-68f0f73222c0"}
[2026-05-11 10:41:17] local.INFO: [EmailSchedule] FINISHED batch process {"host":"docker_lamp_1","processed":0} {"correlation_id":"ee9b9096-b1f1-4e2b-ac17-af0003eb00c0","trace_id":"8904c61b-d5b4-44cc-91bf-68f0f73222c0"}
[2026-05-11 10:41:17] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"mailbox:batch:process","memoryBeforeCommandInMb":60.0,"memoryAfterCommandInMB":60.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"ee9b9096-b1f1-4e2b-ac17-af0003eb00c0","trace_id":"8904c61b-d5b4-44cc-91bf-68f0f73222c0"}
[2026-05-11 10:41:21] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"crm:sync-hubspot-objects","memoryBeforeCommandInMb":60.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"13c8d2de-a598-41db-b4ca-9799a4bee946","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:21] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"crm:sync-hubspot-objects","memoryBeforeCommandInMb":60.0,"memoryAfterCommandInMB":60.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"13c8d2de-a598-41db-b4ca-9799a4bee946","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:21] local.INFO: [SyncHubspotObjects] Starting sync {"team":"b2b115eb-93ce-4d1b-929c-173757df8fba","usage":24894088,"real_usage":65011712,"pid":62058} {"correlation_id":"81433689-3fcd-4128-8b21-068ce5ee75be","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:21] local.WARNING: [HubSpot] Account not connected for user {"userId":"33e34a7a-1c02-4f04-87ac-22c3a385e6e3","account":{"Jiminny\\Models\\SocialAccount":{"id":306,"sociable_id":109,"provider_user_id":"11348452","expires":1701077403,"refresh_token_expires":null,"provider":"hubspot","state":"full-refresh","auth_scope":null,"retry_after":null,"created_at":"2020-09-01 16:59:04","updated_at":"2023-11-27 09:30:03"}}} {"correlation_id":"81433689-3fcd-4128-8b21-068ce5ee75be","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:21] local.INFO: [CrmOwnerResolver] Integration owner is not connected, attempting team members {"crm_provider":"hubspot","crm_owner":109,"team_id":29} {"correlation_id":"81433689-3fcd-4128-8b21-068ce5ee75be","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:21] local.INFO: [CrmOwnerResolver] No team members found with active crm connection {"crm_provider":"hubspot","team_id":29} {"correlation_id":"81433689-3fcd-4128-8b21-068ce5ee75be","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:21] local.INFO: [CrmOwnerResolver] No team member found with active crm connection {"crm_provider":"hubspot","team_id":29} {"correlation_id":"81433689-3fcd-4128-8b21-068ce5ee75be","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:21] local.INFO: [SyncHubspotObjects] Sync finished {"team":"b2b115eb-93ce-4d1b-929c-173757df8fba","provider":"hubspot","status":"disconnected","duration_ms":35.85,"usage":24956128,"real_usage":65011712,"pid":62058,"reason":"Your HubSpot account has become disconnected. Please login to Jiminny to reconnect."} {"correlation_id":"81433689-3fcd-4128-8b21-068ce5ee75be","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:21] local.INFO: [SyncHubspotObjects] Starting sync {"team":"abae74b8-bfa8-4383-9a7f-89f4bf2bdbb4","usage":24914344,"real_usage":65011712,"pid":62058} {"correlation_id":"d2cb4aff-14c0-474f-89f2-f5c69dafe934","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:21] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1499,"provider":"hubspot"} {"correlation_id":"d2cb4aff-14c0-474f-89f2-f5c69dafe934","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:21] local.INFO: [SocialAccountService] Token retrieved {"socialAccountId":1499,"provider":"hubspot"} {"correlation_id":"d2cb4aff-14c0-474f-89f2-f5c69dafe934","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:21] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"d2cb4aff-14c0-474f-89f2-f5c69dafe934","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:21] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {"crm_provider":"hubspot","crm_owner":148,"team_id":2} {"correlation_id":"d2cb4aff-14c0-474f-89f2-f5c69dafe934","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:21] local.INFO: [HubSpot] Syncing opportunities using strategy: lastModified {"team":2} {"correlation_id":"d2cb4aff-14c0-474f-89f2-f5c69dafe934","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:21] local.INFO: [Hubspot] Pagination completed {"team_id":2,"endpoint":"https://api.hubapi.com/crm/v3/objects/deals/search","total_requests":1,"total_records_fetched":0,"total_elapsed_seconds":0.26,"average_seconds_per_request":0.26} {"correlation_id":"d2cb4aff-14c0-474f-89f2-f5c69dafe934","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:21] local.INFO: [HubSpot] Synced opportunities {"team":2,"strategies":"lastModified","sync_count":0,"total":0,"last_synced_id":null,"duration_ms":288.63} {"correlation_id":"d2cb4aff-14c0-474f-89f2-f5c69dafe934","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:21] local.INFO: [SyncHubspotObjects] Sync finished {"team":"abae74b8-bfa8-4383-9a7f-89f4bf2bdbb4","provider":"hubspot","status":"completed","duration_ms":324.99,"usage":24978696,"real_usage":65011712,"pid":62058} {"correlation_id":"d2cb4aff-14c0-474f-89f2-f5c69dafe934","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:22] local.INFO: [SyncHubspotObjects] Starting sync {"team":"c6b9d6b0-b48d-4832-a68c-a57d60651888","usage":24956624,"real_usage":65011712,"pid":62058} {"correlation_id":"955d9400-2f7f-4170-be31-43bafa1ef3bd","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:22] local.WARNING: [HubSpot] Account not connected for user {"userId":"71e3aac5-fb66-47c5-a236-2d051ae3e319","account":null} {"correlation_id":"955d9400-2f7f-4170-be31-43bafa1ef3bd","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:22] local.INFO: [CrmOwnerResolver] Integration owner is not connected, attempting team members {"crm_provider":"hubspot","crm_owner":256,"team_id":49} {"correlation_id":"955d9400-2f7f-4170-be31-43bafa1ef3bd","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:22] local.INFO: [CrmOwnerResolver] No team members found with active crm connection {"crm_provider":"hubspot","team_id":49} {"correlation_id":"955d9400-2f7f-4170-be31-43bafa1ef3bd","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:22] local.INFO: [CrmOwnerResolver] No team member found with active crm connection {"crm_provider":"hubspot","team_id":49} {"correlation_id":"955d9400-2f7f-4170-be31-43bafa1ef3bd","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:22] local.INFO: [SyncHubspotObjects] Sync finished {"team":"c6b9d6b0-b48d-4832-a68c-a57d60651888","provider":"hubspot","status":"disconnected","duration_ms":17.51,"usage":24941048,"real_usage":65011712,"pid":62058,"reason":"Social account for HubSpot cannot be found. Please login to Jiminny to connect."} {"correlation_id":"955d9400-2f7f-4170-be31-43bafa1ef3bd","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:22] local.INFO: [SyncHubspotObjects] Starting sync {"team":"b2d49a54-b645-4637-a7ae-a86cfce6e8e4","usage":24901688,"real_usage":65011712,"pid":62058} {"correlation_id":"6d0b5717-9160-4c2f-ad12-56e42a2c544a","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:22] local.WARNING: [HubSpot] Account not connected for user {"userId":"2ac0447f-3c8c-4ce0-baeb-b63ddb76fa9b","account":null} {"correlation_id":"6d0b5717-9160-4c2f-ad12-56e42a2c544a","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:22] local.INFO: [CrmOwnerResolver] Integration owner is not connected, attempting team members {"crm_provider":"hubspot","crm_owner":130,"team_id":42} {"correlation_id":"6d0b5717-9160-4c2f-ad12-56e42a2c544a","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:22] local.INFO: [CrmOwnerResolver] No team members found with active crm connection {"crm_provider":"hubspot","team_id":42} {"correlation_id":"6d0b5717-9160-4c2f-ad12-56e42a2c544a","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:22] local.INFO: [CrmOwnerResolver] No team member found with active crm connection {"crm_provider":"hubspot","team_id":42} {"correlation_id":"6d0b5717-9160-4c2f-ad12-56e42a2c544a","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:22] local.INFO: [SyncHubspotObjects] Sync finished {"team":"b2d49a54-b645-4637-a7ae-a86cfce6e8e4","provider":"hubspot","status":"disconnected","duration_ms":10.99,"usage":24944632,"real_usage":65011712,"pid":62058,"reason":"Social account for HubSpot cannot be found. Please login to Jiminny to connect."} {"correlation_id":"6d0b5717-9160-4c2f-ad12-56e42a2c544a","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:39] local.INFO: [HubSpot Journal Polling] No data {"correlation_id":"1f8f11e6-0cf5-49dd-b8d7-56f51ec27f8e","trace_id":"0f08076e-448d-487e-99a0-e73dae5c0ad4"}
[2026-05-11 10:41:39] local.WARNING: [HubSpot Journal Polling] Maximum empty results reached, stopping {"empty_results":5,"max_empty_results":5} {"correlation_id":"1f8f11e6-0cf5-49dd-b8d7-56f51ec27f8e","trace_id":"0f08076e-448d-487e-99a0-e73dae5c0ad4"}
[2026-05-11 10:41:39] local.WARNING: [HubSpot Journal Polling] Maximum empty results reached, stopping {"empty_results":5,"max_empty_results":5} {"correlation_id":"1f8f11e6-0cf5-49dd-b8d7-56f51ec27f8e","trace_id":"0f08076e-448d-487e-99a0-e73dae5c0ad4"}
[2026-05-11 10:41:39] local.INFO: [HubSpot Journal Polling] Service ending {"runtime_seconds":57,"total_cycles":5,"files_downloaded":0,"empty_files":0,"other_portal_skipped":0,"total_events":0,"events_per_file":0,"avg_api_ms":296.8,"avg_download_ms":0.0,"avg_transform_ms":0.0,"avg_process_ms":0.0,"peak_memory_mb":99.73} {"correlation_id":"1f8f11e6-0cf5-49dd-b8d7-56f51ec27f8e","trace_id":"0f08076e-448d-487e-99a0-e73dae5c0ad4"}
[2026-05-11 10:41:39] local.INFO: [HubSpot Journal Polling] Saved offset to database on cleanup {"offset":"019e15a9-9ea0-7da7-87bc-82592e3ccf0d"} {"correlation_id":"1f8f11e6-0cf5-49dd-b8d7-56f51ec27f8e","trace_id":"0f08076e-448d-487e-99a0-e73dae5c0ad4"}
[2026-05-11 10:41:39] local.INFO: [HubSpot Journal Polling] Released polling lock {"correlation_id":"1f8f11e6-0cf5-49dd-b8d7-56f51ec27f8e","trace_id":"0f08076e-448d-487e-99a0-e73dae5c0ad4"}
[2026-05-11 10:42:07] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"meeting-bot:schedule-bot","memoryBeforeCommandInMb":60.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"1aa66921-0a71-4d61-8cd7-0663242dca44","trace_id":"f6b1d739-39b6-4043-a81e-a1af2c76d401"}
[2026-05-11 10:42:07] local.INFO: [ScheduleBotCommand] Number of activities to be captured: 0 {"correlation_id":"1aa66921-0a71-4d61-8cd7-0663242dca44","trace_id":"f6b1d739-39b6-4043-a81e-a1af2c76d401"}
[2026-05-11 10:42:07] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"meeting-bot:schedule-bot","memoryBeforeCommandInMb":60.0,"memoryAfterCommandInMB":60.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"1aa66921-0a71-4d61-8cd7-0663242dca44","trace_id":"f6b1d739-39b6-4043-a81e-a1af2c76d401"}
[2026-05-11 10:42:10] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"dialers:monitor-activities","memoryBeforeCommandInMb":60.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"cc420ba5-39bf-4183-95ae-48962a9c124b","trace_id":"8daf6081-5c8a-4fae-98d8-e317d8580bc3"}
[2026-05-11 10:42:10] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"dialers:monitor-activities","memoryBeforeCommandInMb":60.0,"memoryAfterCommandInMB":60.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"cc420ba5-39bf-4183-95ae-48962a9c124b","trace_id":"8daf6081-5c8a-4fae-98d8-e317d8580bc3"}
[2026-05-11 10:42:13] local.NOTICE: Monitoring start {"correlation_id":"50696062-587c-4c43-8075-7229916ea46f","trace_id":"b58f754c-906f-413b-80e6-f89189dbe078"}
[2026-05-11 10:42:13] local.NOTICE: Monitoring end {"correlation_id":"50696062-587c-4c43-8075-7229916ea46f","trace_id":"b58f754c-906f-413b-80e6-f89189dbe078"}
[2026-05-11 10:42:17] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"mailbox:skip-lists:refresh","memoryBeforeCommandInMb":60.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"f70656cd-6de3-4f8b-bb9d-74a941c552c4","trace_id":"c780d8e3-2abd-41a3-a9dc-7c2be6aa22d4"}
[2026-05-11 10:42:17] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"mailbox:skip-lists:refresh","memoryBeforeCommandInMb":60.0,"memoryAfterCommandInMB":60.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"f70656cd-6de3-4f8b-bb9d-74a941c552c4","trace_id":"c780d8e3-2abd-41a3-a9dc-7c2be6aa22d4"}
[2026-05-11 10:42:20] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"mailbox:batch:process","memoryBeforeCommandInMb":60.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"b2e73601-1777-4ddb-a152-fe85f4c89afc","trace_id":"8bac5054-216a-4605-b13b-9efab8ab5341"}
[2026-05-11 10:42:20] local.INFO: [EmailSchedule] STARTING batch process {"host":"docker_lamp_1"} {"correlation_id":"b2e73601-1777-4ddb-a152-fe85f4c89afc","trace_id":"8bac5054-216a-4605-b13b-9efab8ab5341"}
[2026-05-11 10:42:20] local.INFO: [EmailSchedule] FINISHED batch process {"host":"docker_lamp_1","processed":0} {"correlation_i...
|
[{"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":"JY-20725-handle-HS-search-rate-limit, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.09541223,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: JY-20725-handle-HS-search-rate-limit","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.82413566,"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":"HandleHubspotRateLimitTest","depth":6,"bounds":{"left":0.8394282,"top":0.019952115,"width":0.076130316,"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 'HandleHubspotRateLimitTest'","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 'HandleHubspotRateLimitTest'","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":"AXStaticText","text":"3","depth":4,"bounds":{"left":0.5518617,"top":0.15003991,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"70","depth":4,"bounds":{"left":0.5618351,"top":0.15003991,"width":0.009973404,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"3","depth":4,"bounds":{"left":0.5738032,"top":0.15003991,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.5834442,"top":0.14844373,"width":0.00731383,"height":0.018355945},"on_screen":true,"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.59075797,"top":0.14844373,"width":0.006981383,"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\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\\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 Illuminate\\Support\\Facades\\Redis;\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\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Reacts to a rate limits (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 $cacheKey = $this->getRateLimitCacheKey();\n\n $cachedRetryAfter = Redis::get($cacheKey);\n if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n (int) $cachedRetryAfter,\n );\n }\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);\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 'policy' => $this->parsePolicy($e),\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 getRateLimitCacheKey(): string\n {\n return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());\n }\n\n public function isHubspotRateLimit(Throwable $e): bool\n {\n if ($e instanceof BadRequest\n || $e instanceof DealApiException\n || $e instanceof ContactApiException\n || $e instanceof CompanyApiException\n || $e instanceof \\GuzzleHttp\\Exception\\RequestException\n ) {\n return (int) $e->getCode() === 429;\n }\n\n return false;\n }\n\n public function parseRetryAfter(Throwable $e): int\n {\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$e ' . PHP_EOL . print_r($e, true));\n return 5;\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 $policy = $this->parsePolicy($e);\n if ($policy === 'TEN_SECONDLY_ROLLING') {\n return 10;\n }\n if ($policy === 'SECONDLY') {\n return 1;\n }\n if ($policy === 'DAILY_LIMIT') {\n return 600;\n }\n\n $this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [\n 'exception_class' => get_class($e),\n ]);\n\n return 10;\n }\n\n public function parsePolicy(Throwable $e): ?string\n {\n if (! method_exists($e, 'getResponseBody')) {\n return null;\n }\n\n $body = $e->getResponseBody();\n if (is_string($body)) {\n $body = json_decode($body, true) ?? [];\n }\n\n if (! is_array($body)) {\n return null;\n }\n\n $policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;\n\n return is_string($policy) ? strtoupper($policy) : null;\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 * Execute a search request against HubSpot CRM objects with rate limiting.\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n * @return array The search response with 'results', 'total', 'paging' keys\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n */\n public function search(string $objectType, array $payload): array\n {\n $endpoint = self::BASE_URL . \"/crm/v3/objects/{$objectType}/search\";\n\n return $this->executeRequest(function () use ($endpoint, $payload) {\n $response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$response ' . PHP_EOL . print_r($response, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$headers ' . PHP_EOL . print_r($response->getHeaders(), true));\n\n return $response->toArray();\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 $deal = $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 return $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n return $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\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 (RateLimitException $e) {\n// throw $e;\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\\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 Illuminate\\Support\\Facades\\Redis;\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\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Reacts to a rate limits (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 $cacheKey = $this->getRateLimitCacheKey();\n\n $cachedRetryAfter = Redis::get($cacheKey);\n if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n (int) $cachedRetryAfter,\n );\n }\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);\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 'policy' => $this->parsePolicy($e),\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 getRateLimitCacheKey(): string\n {\n return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());\n }\n\n public function isHubspotRateLimit(Throwable $e): bool\n {\n if ($e instanceof BadRequest\n || $e instanceof DealApiException\n || $e instanceof ContactApiException\n || $e instanceof CompanyApiException\n || $e instanceof \\GuzzleHttp\\Exception\\RequestException\n ) {\n return (int) $e->getCode() === 429;\n }\n\n return false;\n }\n\n public function parseRetryAfter(Throwable $e): int\n {\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$e ' . PHP_EOL . print_r($e, true));\n return 5;\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 $policy = $this->parsePolicy($e);\n if ($policy === 'TEN_SECONDLY_ROLLING') {\n return 10;\n }\n if ($policy === 'SECONDLY') {\n return 1;\n }\n if ($policy === 'DAILY_LIMIT') {\n return 600;\n }\n\n $this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [\n 'exception_class' => get_class($e),\n ]);\n\n return 10;\n }\n\n public function parsePolicy(Throwable $e): ?string\n {\n if (! method_exists($e, 'getResponseBody')) {\n return null;\n }\n\n $body = $e->getResponseBody();\n if (is_string($body)) {\n $body = json_decode($body, true) ?? [];\n }\n\n if (! is_array($body)) {\n return null;\n }\n\n $policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;\n\n return is_string($policy) ? strtoupper($policy) : null;\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 * Execute a search request against HubSpot CRM objects with rate limiting.\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n * @return array The search response with 'results', 'total', 'paging' keys\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n */\n public function search(string $objectType, array $payload): array\n {\n $endpoint = self::BASE_URL . \"/crm/v3/objects/{$objectType}/search\";\n\n return $this->executeRequest(function () use ($endpoint, $payload) {\n $response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$response ' . PHP_EOL . print_r($response, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$headers ' . PHP_EOL . print_r($response->getHeaders(), true));\n\n return $response->toArray();\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 $deal = $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 return $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n return $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\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 (RateLimitException $e) {\n// throw $e;\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":"AXButton","text":"Show Replace Field","depth":4,"bounds":{"left":0.60206115,"top":0.08060654,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Search History","depth":3,"bounds":{"left":0.6146942,"top":0.07980846,"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":"Received 429 from API","depth":4,"bounds":{"left":0.6256649,"top":0.07980846,"width":0.0631649,"height":0.015961692},"on_screen":true,"value":"Received 429 from API","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"New Line","depth":3,"bounds":{"left":0.6978058,"top":0.07980846,"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":3,"bounds":{"left":0.7077792,"top":0.07980846,"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":3,"bounds":{"left":0.71642286,"top":0.07980846,"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":3,"bounds":{"left":0.7250665,"top":0.07980846,"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":3,"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":4,"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":3,"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":3,"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":"AXStaticText","text":"0 results","depth":4,"bounds":{"left":0.7386968,"top":0.079010375,"width":0.025598405,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Occurrence","depth":4,"bounds":{"left":0.7642952,"top":0.07821229,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Occurrence","depth":4,"bounds":{"left":0.77293885,"top":0.07821229,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Filter Search Results","depth":4,"bounds":{"left":0.7815825,"top":0.07821229,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Open in Window, Multiple Cursors","depth":4,"bounds":{"left":0.79022604,"top":0.07821229,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Click to highlight","depth":4,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close","depth":4,"bounds":{"left":0.97539896,"top":0.07821229,"width":0.008643617,"height":0.01915403},"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":"[2026-05-11 10:41:16] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"ee9b9096-b1f1-4e2b-ac17-af0003eb00c0\",\"trace_id\":\"8904c61b-d5b4-44cc-91bf-68f0f73222c0\"}\n[2026-05-11 10:41:16] local.INFO: [EmailSchedule] STARTING batch process {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"ee9b9096-b1f1-4e2b-ac17-af0003eb00c0\",\"trace_id\":\"8904c61b-d5b4-44cc-91bf-68f0f73222c0\"}\n[2026-05-11 10:41:17] local.INFO: [EmailSchedule] FINISHED batch process {\"host\":\"docker_lamp_1\",\"processed\":0} {\"correlation_id\":\"ee9b9096-b1f1-4e2b-ac17-af0003eb00c0\",\"trace_id\":\"8904c61b-d5b4-44cc-91bf-68f0f73222c0\"}\n[2026-05-11 10:41:17] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":60.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"ee9b9096-b1f1-4e2b-ac17-af0003eb00c0\",\"trace_id\":\"8904c61b-d5b4-44cc-91bf-68f0f73222c0\"}\n[2026-05-11 10:41:21] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"crm:sync-hubspot-objects\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"13c8d2de-a598-41db-b4ca-9799a4bee946\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:21] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"crm:sync-hubspot-objects\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":60.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"13c8d2de-a598-41db-b4ca-9799a4bee946\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:21] local.INFO: [SyncHubspotObjects] Starting sync {\"team\":\"b2b115eb-93ce-4d1b-929c-173757df8fba\",\"usage\":24894088,\"real_usage\":65011712,\"pid\":62058} {\"correlation_id\":\"81433689-3fcd-4128-8b21-068ce5ee75be\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:21] local.WARNING: [HubSpot] Account not connected for user {\"userId\":\"33e34a7a-1c02-4f04-87ac-22c3a385e6e3\",\"account\":{\"Jiminny\\\\Models\\\\SocialAccount\":{\"id\":306,\"sociable_id\":109,\"provider_user_id\":\"11348452\",\"expires\":1701077403,\"refresh_token_expires\":null,\"provider\":\"hubspot\",\"state\":\"full-refresh\",\"auth_scope\":null,\"retry_after\":null,\"created_at\":\"2020-09-01 16:59:04\",\"updated_at\":\"2023-11-27 09:30:03\"}}} {\"correlation_id\":\"81433689-3fcd-4128-8b21-068ce5ee75be\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:21] local.INFO: [CrmOwnerResolver] Integration owner is not connected, attempting team members {\"crm_provider\":\"hubspot\",\"crm_owner\":109,\"team_id\":29} {\"correlation_id\":\"81433689-3fcd-4128-8b21-068ce5ee75be\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:21] local.INFO: [CrmOwnerResolver] No team members found with active crm connection {\"crm_provider\":\"hubspot\",\"team_id\":29} {\"correlation_id\":\"81433689-3fcd-4128-8b21-068ce5ee75be\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:21] local.INFO: [CrmOwnerResolver] No team member found with active crm connection {\"crm_provider\":\"hubspot\",\"team_id\":29} {\"correlation_id\":\"81433689-3fcd-4128-8b21-068ce5ee75be\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:21] local.INFO: [SyncHubspotObjects] Sync finished {\"team\":\"b2b115eb-93ce-4d1b-929c-173757df8fba\",\"provider\":\"hubspot\",\"status\":\"disconnected\",\"duration_ms\":35.85,\"usage\":24956128,\"real_usage\":65011712,\"pid\":62058,\"reason\":\"Your HubSpot account has become disconnected. Please login to Jiminny to reconnect.\"} {\"correlation_id\":\"81433689-3fcd-4128-8b21-068ce5ee75be\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:21] local.INFO: [SyncHubspotObjects] Starting sync {\"team\":\"abae74b8-bfa8-4383-9a7f-89f4bf2bdbb4\",\"usage\":24914344,\"real_usage\":65011712,\"pid\":62058} {\"correlation_id\":\"d2cb4aff-14c0-474f-89f2-f5c69dafe934\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:21] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"d2cb4aff-14c0-474f-89f2-f5c69dafe934\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:21] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"d2cb4aff-14c0-474f-89f2-f5c69dafe934\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:21] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d2cb4aff-14c0-474f-89f2-f5c69dafe934\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:21] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"d2cb4aff-14c0-474f-89f2-f5c69dafe934\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:21] local.INFO: [HubSpot] Syncing opportunities using strategy: lastModified {\"team\":2} {\"correlation_id\":\"d2cb4aff-14c0-474f-89f2-f5c69dafe934\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:21] local.INFO: [Hubspot] Pagination completed {\"team_id\":2,\"endpoint\":\"https://api.hubapi.com/crm/v3/objects/deals/search\",\"total_requests\":1,\"total_records_fetched\":0,\"total_elapsed_seconds\":0.26,\"average_seconds_per_request\":0.26} {\"correlation_id\":\"d2cb4aff-14c0-474f-89f2-f5c69dafe934\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:21] local.INFO: [HubSpot] Synced opportunities {\"team\":2,\"strategies\":\"lastModified\",\"sync_count\":0,\"total\":0,\"last_synced_id\":null,\"duration_ms\":288.63} {\"correlation_id\":\"d2cb4aff-14c0-474f-89f2-f5c69dafe934\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:21] local.INFO: [SyncHubspotObjects] Sync finished {\"team\":\"abae74b8-bfa8-4383-9a7f-89f4bf2bdbb4\",\"provider\":\"hubspot\",\"status\":\"completed\",\"duration_ms\":324.99,\"usage\":24978696,\"real_usage\":65011712,\"pid\":62058} {\"correlation_id\":\"d2cb4aff-14c0-474f-89f2-f5c69dafe934\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:22] local.INFO: [SyncHubspotObjects] Starting sync {\"team\":\"c6b9d6b0-b48d-4832-a68c-a57d60651888\",\"usage\":24956624,\"real_usage\":65011712,\"pid\":62058} {\"correlation_id\":\"955d9400-2f7f-4170-be31-43bafa1ef3bd\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:22] local.WARNING: [HubSpot] Account not connected for user {\"userId\":\"71e3aac5-fb66-47c5-a236-2d051ae3e319\",\"account\":null} {\"correlation_id\":\"955d9400-2f7f-4170-be31-43bafa1ef3bd\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:22] local.INFO: [CrmOwnerResolver] Integration owner is not connected, attempting team members {\"crm_provider\":\"hubspot\",\"crm_owner\":256,\"team_id\":49} {\"correlation_id\":\"955d9400-2f7f-4170-be31-43bafa1ef3bd\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:22] local.INFO: [CrmOwnerResolver] No team members found with active crm connection {\"crm_provider\":\"hubspot\",\"team_id\":49} {\"correlation_id\":\"955d9400-2f7f-4170-be31-43bafa1ef3bd\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:22] local.INFO: [CrmOwnerResolver] No team member found with active crm connection {\"crm_provider\":\"hubspot\",\"team_id\":49} {\"correlation_id\":\"955d9400-2f7f-4170-be31-43bafa1ef3bd\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:22] local.INFO: [SyncHubspotObjects] Sync finished {\"team\":\"c6b9d6b0-b48d-4832-a68c-a57d60651888\",\"provider\":\"hubspot\",\"status\":\"disconnected\",\"duration_ms\":17.51,\"usage\":24941048,\"real_usage\":65011712,\"pid\":62058,\"reason\":\"Social account for HubSpot cannot be found. Please login to Jiminny to connect.\"} {\"correlation_id\":\"955d9400-2f7f-4170-be31-43bafa1ef3bd\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:22] local.INFO: [SyncHubspotObjects] Starting sync {\"team\":\"b2d49a54-b645-4637-a7ae-a86cfce6e8e4\",\"usage\":24901688,\"real_usage\":65011712,\"pid\":62058} {\"correlation_id\":\"6d0b5717-9160-4c2f-ad12-56e42a2c544a\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:22] local.WARNING: [HubSpot] Account not connected for user {\"userId\":\"2ac0447f-3c8c-4ce0-baeb-b63ddb76fa9b\",\"account\":null} {\"correlation_id\":\"6d0b5717-9160-4c2f-ad12-56e42a2c544a\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:22] local.INFO: [CrmOwnerResolver] Integration owner is not connected, attempting team members {\"crm_provider\":\"hubspot\",\"crm_owner\":130,\"team_id\":42} {\"correlation_id\":\"6d0b5717-9160-4c2f-ad12-56e42a2c544a\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:22] local.INFO: [CrmOwnerResolver] No team members found with active crm connection {\"crm_provider\":\"hubspot\",\"team_id\":42} {\"correlation_id\":\"6d0b5717-9160-4c2f-ad12-56e42a2c544a\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:22] local.INFO: [CrmOwnerResolver] No team member found with active crm connection {\"crm_provider\":\"hubspot\",\"team_id\":42} {\"correlation_id\":\"6d0b5717-9160-4c2f-ad12-56e42a2c544a\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:22] local.INFO: [SyncHubspotObjects] Sync finished {\"team\":\"b2d49a54-b645-4637-a7ae-a86cfce6e8e4\",\"provider\":\"hubspot\",\"status\":\"disconnected\",\"duration_ms\":10.99,\"usage\":24944632,\"real_usage\":65011712,\"pid\":62058,\"reason\":\"Social account for HubSpot cannot be found. Please login to Jiminny to connect.\"} {\"correlation_id\":\"6d0b5717-9160-4c2f-ad12-56e42a2c544a\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:39] local.INFO: [HubSpot Journal Polling] No data {\"correlation_id\":\"1f8f11e6-0cf5-49dd-b8d7-56f51ec27f8e\",\"trace_id\":\"0f08076e-448d-487e-99a0-e73dae5c0ad4\"}\n[2026-05-11 10:41:39] local.WARNING: [HubSpot Journal Polling] Maximum empty results reached, stopping {\"empty_results\":5,\"max_empty_results\":5} {\"correlation_id\":\"1f8f11e6-0cf5-49dd-b8d7-56f51ec27f8e\",\"trace_id\":\"0f08076e-448d-487e-99a0-e73dae5c0ad4\"}\n[2026-05-11 10:41:39] local.WARNING: [HubSpot Journal Polling] Maximum empty results reached, stopping {\"empty_results\":5,\"max_empty_results\":5} {\"correlation_id\":\"1f8f11e6-0cf5-49dd-b8d7-56f51ec27f8e\",\"trace_id\":\"0f08076e-448d-487e-99a0-e73dae5c0ad4\"}\n[2026-05-11 10:41:39] local.INFO: [HubSpot Journal Polling] Service ending {\"runtime_seconds\":57,\"total_cycles\":5,\"files_downloaded\":0,\"empty_files\":0,\"other_portal_skipped\":0,\"total_events\":0,\"events_per_file\":0,\"avg_api_ms\":296.8,\"avg_download_ms\":0.0,\"avg_transform_ms\":0.0,\"avg_process_ms\":0.0,\"peak_memory_mb\":99.73} {\"correlation_id\":\"1f8f11e6-0cf5-49dd-b8d7-56f51ec27f8e\",\"trace_id\":\"0f08076e-448d-487e-99a0-e73dae5c0ad4\"}\n[2026-05-11 10:41:39] local.INFO: [HubSpot Journal Polling] Saved offset to database on cleanup {\"offset\":\"019e15a9-9ea0-7da7-87bc-82592e3ccf0d\"} {\"correlation_id\":\"1f8f11e6-0cf5-49dd-b8d7-56f51ec27f8e\",\"trace_id\":\"0f08076e-448d-487e-99a0-e73dae5c0ad4\"}\n[2026-05-11 10:41:39] local.INFO: [HubSpot Journal Polling] Released polling lock {\"correlation_id\":\"1f8f11e6-0cf5-49dd-b8d7-56f51ec27f8e\",\"trace_id\":\"0f08076e-448d-487e-99a0-e73dae5c0ad4\"}\n[2026-05-11 10:42:07] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"1aa66921-0a71-4d61-8cd7-0663242dca44\",\"trace_id\":\"f6b1d739-39b6-4043-a81e-a1af2c76d401\"}\n[2026-05-11 10:42:07] local.INFO: [ScheduleBotCommand] Number of activities to be captured: 0 {\"correlation_id\":\"1aa66921-0a71-4d61-8cd7-0663242dca44\",\"trace_id\":\"f6b1d739-39b6-4043-a81e-a1af2c76d401\"}\n[2026-05-11 10:42:07] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":60.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"1aa66921-0a71-4d61-8cd7-0663242dca44\",\"trace_id\":\"f6b1d739-39b6-4043-a81e-a1af2c76d401\"}\n[2026-05-11 10:42:10] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"cc420ba5-39bf-4183-95ae-48962a9c124b\",\"trace_id\":\"8daf6081-5c8a-4fae-98d8-e317d8580bc3\"}\n[2026-05-11 10:42:10] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":60.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"cc420ba5-39bf-4183-95ae-48962a9c124b\",\"trace_id\":\"8daf6081-5c8a-4fae-98d8-e317d8580bc3\"}\n[2026-05-11 10:42:13] local.NOTICE: Monitoring start {\"correlation_id\":\"50696062-587c-4c43-8075-7229916ea46f\",\"trace_id\":\"b58f754c-906f-413b-80e6-f89189dbe078\"}\n[2026-05-11 10:42:13] local.NOTICE: Monitoring end {\"correlation_id\":\"50696062-587c-4c43-8075-7229916ea46f\",\"trace_id\":\"b58f754c-906f-413b-80e6-f89189dbe078\"}\n[2026-05-11 10:42:17] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"f70656cd-6de3-4f8b-bb9d-74a941c552c4\",\"trace_id\":\"c780d8e3-2abd-41a3-a9dc-7c2be6aa22d4\"}\n[2026-05-11 10:42:17] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":60.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"f70656cd-6de3-4f8b-bb9d-74a941c552c4\",\"trace_id\":\"c780d8e3-2abd-41a3-a9dc-7c2be6aa22d4\"}\n[2026-05-11 10:42:20] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"b2e73601-1777-4ddb-a152-fe85f4c89afc\",\"trace_id\":\"8bac5054-216a-4605-b13b-9efab8ab5341\"}\n[2026-05-11 10:42:20] local.INFO: [EmailSchedule] STARTING batch process {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"b2e73601-1777-4ddb-a152-fe85f4c89afc\",\"trace_id\":\"8bac5054-216a-4605-b13b-9efab8ab5341\"}\n[2026-05-11 10:42:20] local.INFO: [EmailSchedule] FINISHED batch process {\"host\":\"docker_lamp_1\",\"processed\":0} {\"correlation_id\":\"b2e73601-1777-4ddb-a152-fe85f4c89afc\",\"trace_id\":\"8bac5054-216a-4605-b13b-9efab8ab5341\"}\n[2026-05-11 10:42:20] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":60.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"b2e73601-1777-4ddb-a152-fe85f4c89afc\",\"trace_id\":\"8bac5054-216a-4605-b13b-9efab8ab5341\"}\n[2026-05-11 10:42:31] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"conference:monitor:count\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"175d1973-e4ab-47f7-bb25-14bc3f0a1cce\",\"trace_id\":\"723f475a-f2fc-4235-8163-c2a8ed5777e6\"}\n[2026-05-11 10:42:31] local.INFO: Running conference:monitor:count command for activities in (2026-05-11 10:40:00, 2026-05-11 10:42:00] {\"correlation_id\":\"175d1973-e4ab-47f7-bb25-14bc3f0a1cce\",\"trace_id\":\"723f475a-f2fc-4235-8163-c2a8ed5777e6\"}\n[2026-05-11 10:42:31] local.INFO: [conference:monitor:count] No activities found in (2026-05-11 10:40:00, 2026-05-11 10:42:00] {\"correlation_id\":\"175d1973-e4ab-47f7-bb25-14bc3f0a1cce\",\"trace_id\":\"723f475a-f2fc-4235-8163-c2a8ed5777e6\"}\n[2026-05-11 10:42:31] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"conference:monitor:count\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":60.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"175d1973-e4ab-47f7-bb25-14bc3f0a1cce\",\"trace_id\":\"723f475a-f2fc-4235-8163-c2a8ed5777e6\"}\n[2026-05-11 10:42:36] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:create\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"4243f43b-9d70-4dd0-9c98-3122eb9445ed\",\"trace_id\":\"0c811e1d-ed7e-4aa7-ba81-a14a0cf4b306\"}\n[2026-05-11 10:42:36] local.INFO: [EmailSchedule] STARTING batch create {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"4243f43b-9d70-4dd0-9c98-3122eb9445ed\",\"trace_id\":\"0c811e1d-ed7e-4aa7-ba81-a14a0cf4b306\"}\n[2026-05-11 10:42:36] local.INFO: [EmailSchedule] FINISHED batch create {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"4243f43b-9d70-4dd0-9c98-3122eb9445ed\",\"trace_id\":\"0c811e1d-ed7e-4aa7-ba81-a14a0cf4b306\"}\n[2026-05-11 10:42:36] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:create\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":60.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"4243f43b-9d70-4dd0-9c98-3122eb9445ed\",\"trace_id\":\"0c811e1d-ed7e-4aa7-ba81-a14a0cf4b306\"}\n[2026-05-11 10:42:39] local.INFO: [Jiminny\\Jobs\\Mailbox\\CreateBatches] processed 2 inboxes and created 0 batches {\"userId\":null,\"batchSize\":30,\"maxBatches\":1000} {\"correlation_id\":\"2328b74c-d9ea-43ef-95db-20ea7c6ec9f5\",\"trace_id\":\"0c811e1d-ed7e-4aa7-ba81-a14a0cf4b306\"}\n[2026-05-11 10:42:54] local.INFO: [HubSpot Webhook] Signature validation started {\"method\":\"POST\",\"uri\":\"/webhook/conference/hubspot/events\",\"has_v3_signature\":true,\"has_v1_signature\":true} {\"correlation_id\":\"35391537-e747-4658-94f7-9d3d8c65eab6\",\"trace_id\":\"163e44b3-9f8e-4957-b04d-4937c33d30e6\"}\n[2026-05-11 10:42:54] local.INFO: [HubSpot Webhook] Event received {\"payload\":[{\"eventId\":1531490889,\"subscriptionId\":5683273,\"portalId\":4392066,\"appId\":38482,\"occurredAt\":1778478181931,\"subscriptionType\":\"company.associationChange\",\"attemptNumber\":8,\"changeSource\":\"USER\",\"associationType\":\"COMPANY_TO_DEAL\",\"fromObjectId\":13266161465,\"toObjectId\":60142058500,\"associationRemoved\":false,\"isPrimaryAssociation\":false}],\"event_count\":1} {\"correlation_id\":\"35391537-e747-4658-94f7-9d3d8c65eab6\",\"trace_id\":\"163e44b3-9f8e-4957-b04d-4937c33d30e6\"}\n[2026-05-11 10:42:54] local.INFO: [HubSpot Webhook Job] Processing webhook events {\"event_count\":1,\"local_count\":1,\"forward_count\":1} {\"correlation_id\":\"bb5ee15c-0826-40ef-a4c4-83761c307c7b\",\"trace_id\":\"163e44b3-9f8e-4957-b04d-4937c33d30e6\"}\n[2026-05-11 10:42:55] local.INFO: [Webhook Forwarder] Forwarding to instance {\"url\":\"https://uranus.staging.jiminny.com/internal/webhook-receiver/hubspot\",\"route\":\"internal.webhook-receiver.hubspot\",\"event_count\":1} {\"correlation_id\":\"bb5ee15c-0826-40ef-a4c4-83761c307c7b\",\"trace_id\":\"163e44b3-9f8e-4957-b04d-4937c33d30e6\"}\n[2026-05-11 10:42:55] local.ERROR: [Webhook Forwarder] Failed to forward to instance {\"url\":\"https://uranus.staging.jiminny.com\",\"route\":\"internal.webhook-receiver.hubspot\",\"error\":\"Server error: `POST https://uranus.staging.jiminny.com/internal/webhook-receiver/hubspot` resulted in a `503 Service Temporarily Unavailable` response:\n<html>\n\n<head><title>503 Service Temporarily Unavailable</title></head>\n\n<body>\n\n<center><h1>503 Service Temporarily Una (truncated...)\n\",\"code\":503} {\"correlation_id\":\"bb5ee15c-0826-40ef-a4c4-83761c307c7b\",\"trace_id\":\"163e44b3-9f8e-4957-b04d-4937c33d30e6\"}\n[2026-05-11 10:42:55] local.INFO: [Webhook Forwarder] Forwarding to instance {\"url\":\"https://app.qai.jiminny.com/internal/webhook-receiver/hubspot\",\"route\":\"internal.webhook-receiver.hubspot\",\"event_count\":1} {\"correlation_id\":\"bb5ee15c-0826-40ef-a4c4-83761c307c7b\",\"trace_id\":\"163e44b3-9f8e-4957-b04d-4937c33d30e6\"}\n[2026-05-11 10:42:56] local.INFO: [Webhook Forwarder] Successfully forwarded to instance {\"url\":\"https://app.qai.jiminny.com/internal/webhook-receiver/hubspot\",\"status_code\":202,\"response\":{\"status\":\"accepted\",\"message\":\"Events queued for processing\",\"event_count\":1}} {\"correlation_id\":\"bb5ee15c-0826-40ef-a4c4-83761c307c7b\",\"trace_id\":\"163e44b3-9f8e-4957-b04d-4937c33d30e6\"}\n[2026-05-11 10:42:56] local.INFO: [BatchSyncCollector] Stored webhook in Redis {\"object_type\":\"company\",\"crm_provider_id\":\"13266161465\",\"event_type\":\"association_change\",\"configuration_id\":2,\"batch_key\":\"batch_sync_company:ids:2:all\",\"was_new\":true,\"current_size\":1} {\"correlation_id\":\"bb5ee15c-0826-40ef-a4c4-83761c307c7b\",\"trace_id\":\"163e44b3-9f8e-4957-b04d-4937c33d30e6\"}\n[2026-05-11 10:43:14] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"a2a4326d-0416-4ea7-8eac-370e0fee93fb\",\"trace_id\":\"49cf2405-4c7b-48ce-9aac-074f08037e94\"}\n[2026-05-11 10:43:14] local.INFO: [ScheduleBotCommand] Number of activities to be captured: 0 {\"correlation_id\":\"a2a4326d-0416-4ea7-8eac-370e0fee93fb\",\"trace_id\":\"49cf2405-4c7b-48ce-9aac-074f08037e94\"}\n[2026-05-11 10:43:14] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":60.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"a2a4326d-0416-4ea7-8eac-370e0fee93fb\",\"trace_id\":\"49cf2405-4c7b-48ce-9aac-074f08037e94\"}\n[2026-05-11 10:43:18] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":615092,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"e7be542b-ee51-44b8-a563-a2eac45b3413\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:18] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":615092} {\"correlation_id\":\"e7be542b-ee51-44b8-a563-a2eac45b3413\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:18] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":615092,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"e7be542b-ee51-44b8-a563-a2eac45b3413\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:18] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":615092,\"participants\":[{\"id\":1004102,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":1004103,\"user_id\":89,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"e7be542b-ee51-44b8-a563-a2eac45b3413\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:19] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"e7be542b-ee51-44b8-a563-a2eac45b3413\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:19] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"e7be542b-ee51-44b8-a563-a2eac45b3413\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:19] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"e7be542b-ee51-44b8-a563-a2eac45b3413\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:19] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"e7be542b-ee51-44b8-a563-a2eac45b3413\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:20] local.INFO: [Prospect match] Cache miss, calling the API {\"identifier_type\":\"email\",\"identifier\":\"nikolay.nikolov@jiminny.com\"} {\"correlation_id\":\"e7be542b-ee51-44b8-a563-a2eac45b3413\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:20] local.INFO: [Hubspot] Failed to fetch contact {\"email\":\"nikolay.nikolov@jiminny.com\",\"reason\":\"[404] Client error: `GET https://api.hubapi.com/crm/v3/objects/contacts/nikolay.nikolov%40jiminny.com?properties=email%2Cfirstname%2Clastname%2Ccountry%2Cphone%2Cmobilephone%2Cjobtitle%2Chubspot_owner_id%2Cassociatedcompanyid%2Cphoto&archived=0&idProperty=email` resulted in a `404 Not Found` response\"} {\"correlation_id\":\"e7be542b-ee51-44b8-a563-a2eac45b3413\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:20] local.INFO: [Prospect match] API returned empty result, caching the miss with empty prospect data {\"identifier_type\":\"email\",\"identifier\":\"nikolay.nikolov@jiminny.com\"} {\"correlation_id\":\"e7be542b-ee51-44b8-a563-a2eac45b3413\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:21] local.INFO: [Hubspot] Pagination completed {\"team_id\":2,\"endpoint\":\"https://api.hubapi.com/crm/v3/objects/contact/search\",\"total_requests\":1,\"total_records_fetched\":0,\"total_elapsed_seconds\":0.38,\"average_seconds_per_request\":0.38} {\"correlation_id\":\"e7be542b-ee51-44b8-a563-a2eac45b3413\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:22] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"ab7124d8-7931-41ef-8341-93a3305f65eb\",\"trace_id\":\"4564d24d-bf9e-486b-ac8e-0f69c9b286d8\"}\n[2026-05-11 10:43:22] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":60.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"ab7124d8-7931-41ef-8341-93a3305f65eb\",\"trace_id\":\"4564d24d-bf9e-486b-ac8e-0f69c9b286d8\"}\n[2026-05-11 10:43:22] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"nikolay.nikolov@jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"e7be542b-ee51-44b8-a563-a2eac45b3413\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:22] local.INFO: [Prospect match] Cache miss {\"identifier_type\":\"domain\",\"identifier\":\"jiminny.com\",\"crm\":\"hubspot\"} {\"correlation_id\":\"e7be542b-ee51-44b8-a563-a2eac45b3413\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:22] local.INFO: [Prospect match] Cache miss, calling the API {\"identifier_type\":\"domain\",\"identifier\":\"jiminny.com\"} {\"correlation_id\":\"e7be542b-ee51-44b8-a563-a2eac45b3413\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:23] local.INFO: [HubSpot] importAccount {\"crm_provider_id\":\"749766179\",\"config_id\":2} {\"correlation_id\":\"e7be542b-ee51-44b8-a563-a2eac45b3413\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:23] local.INFO: [HubSpot] CRM Search requested {\"request\":{\"filterGroups\":[{\"filters\":[{\"propertyName\":\"associations.company\",\"operator\":\"EQ\",\"value\":\"749766179\"},{\"propertyName\":\"dealstage\",\"operator\":\"NOT_IN\",\"values\":[\"closedwon\",\"4040964\",\"59247967\"]},{\"propertyName\":\"dealstage\",\"operator\":\"NOT_IN\",\"values\":[\"closedlost\",\"4040965\",\"59247968\"]}]}],\"sorts\":[{\"propertyName\":\"modifieddate\",\"direction\":\"DESCENDING\"}],\"properties\":[\"dealname\",\"amount\",\"hubspot_owner_id\",\"pipeline\",\"dealstage\",\"closedate\",\"deal_currency_code\"],\"limit\":200}} {\"correlation_id\":\"e7be542b-ee51-44b8-a563-a2eac45b3413\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:23] local.INFO: [Hubspot] Pagination completed {\"team_id\":2,\"endpoint\":\"https://api.hubapi.com/crm/v3/objects/deals/search\",\"total_requests\":1,\"total_records_fetched\":10,\"total_elapsed_seconds\":0.3,\"average_seconds_per_request\":0.3} {\"correlation_id\":\"e7be542b-ee51-44b8-a563-a2eac45b3413\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:24] local.INFO: [ EsUpdateProcessManager ] Finished updating entities in ES {\"worker\":\"\",\"peak_memory\":\"99.73 MB\",\"elapsed_seconds\":3.22,\"update_target\":\"activities\",\"should_iterate_again\":false} {\"correlation_id\":\"4ad05333-9afb-492e-9f0f-b2909ac45b32\",\"trace_id\":\"3d8feb24-b173-4158-b0a4-4cf33af85066\"}\n[2026-05-11 10:43:24] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":615092,\"team_id\":2,\"email\":\"nikolay.nikolov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"e7be542b-ee51-44b8-a563-a2eac45b3413\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:24] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":615092,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"e7be542b-ee51-44b8-a563-a2eac45b3413\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:24] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":615092} {\"correlation_id\":\"e7be542b-ee51-44b8-a563-a2eac45b3413\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:24] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":615092,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"e7be542b-ee51-44b8-a563-a2eac45b3413\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:24] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":615092,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"e7be542b-ee51-44b8-a563-a2eac45b3413\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:24] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":614436,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"a34067d1-038f-48bf-af25-c9e09374ce62\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:24] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":614436} {\"correlation_id\":\"a34067d1-038f-48bf-af25-c9e09374ce62\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:24] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":614436,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"a34067d1-038f-48bf-af25-c9e09374ce62\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:24] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":614436,\"participants\":[{\"id\":1002751,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":1002752,\"user_id\":89,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"a34067d1-038f-48bf-af25-c9e09374ce62\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:24] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"a34067d1-038f-48bf-af25-c9e09374ce62\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:24] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"a34067d1-038f-48bf-af25-c9e09374ce62\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:24] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"a34067d1-038f-48bf-af25-c9e09374ce62\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:24] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"a34067d1-038f-48bf-af25-c9e09374ce62\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:24] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"nikolay.nikolov@jiminny.com\"} {\"correlation_id\":\"a34067d1-038f-48bf-af25-c9e09374ce62\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:24] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"nikolay.nikolov@jiminny.com\"} {\"correlation_id\":\"a34067d1-038f-48bf-af25-c9e09374ce62\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:24] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"nikolay.nikolov@jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"a34067d1-038f-48bf-af25-c9e09374ce62\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:24] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":614436,\"team_id\":2,\"email\":\"nikolay.nikolov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"a34067d1-038f-48bf-af25-c9e09374ce62\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:24] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":614436,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"a34067d1-038f-48bf-af25-c9e09374ce62\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:24] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":614436} {\"correlation_id\":\"a34067d1-038f-48bf-af25-c9e09374ce62\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:24] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":614436,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"a34067d1-038f-48bf-af25-c9e09374ce62\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:24] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":614436,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"a34067d1-038f-48bf-af25-c9e09374ce62\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":614382,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"ee88af0b-20d3-4911-a378-bea7fc6dc3f6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":614382} {\"correlation_id\":\"ee88af0b-20d3-4911-a378-bea7fc6dc3f6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":614382,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"ee88af0b-20d3-4911-a378-bea7fc6dc3f6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":614382,\"participants\":[{\"id\":1002632,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":1002633,\"user_id\":89,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"ee88af0b-20d3-4911-a378-bea7fc6dc3f6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"ee88af0b-20d3-4911-a378-bea7fc6dc3f6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"ee88af0b-20d3-4911-a378-bea7fc6dc3f6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"ee88af0b-20d3-4911-a378-bea7fc6dc3f6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"ee88af0b-20d3-4911-a378-bea7fc6dc3f6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"nikolay.nikolov@jiminny.com\"} {\"correlation_id\":\"ee88af0b-20d3-4911-a378-bea7fc6dc3f6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"nikolay.nikolov@jiminny.com\"} {\"correlation_id\":\"ee88af0b-20d3-4911-a378-bea7fc6dc3f6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"nikolay.nikolov@jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"ee88af0b-20d3-4911-a378-bea7fc6dc3f6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":614382,\"team_id\":2,\"email\":\"nikolay.nikolov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"ee88af0b-20d3-4911-a378-bea7fc6dc3f6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":614382,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"ee88af0b-20d3-4911-a378-bea7fc6dc3f6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":614382} {\"correlation_id\":\"ee88af0b-20d3-4911-a378-bea7fc6dc3f6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":614382,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"ee88af0b-20d3-4911-a378-bea7fc6dc3f6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":614382,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"ee88af0b-20d3-4911-a378-bea7fc6dc3f6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":614381,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"535b99f0-d38e-4b9c-bf19-3751ce4e6a1d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":614381} {\"correlation_id\":\"535b99f0-d38e-4b9c-bf19-3751ce4e6a1d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":614381,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"535b99f0-d38e-4b9c-bf19-3751ce4e6a1d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":614381,\"participants\":[{\"id\":1002630,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":1002631,\"user_id\":89,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"535b99f0-d38e-4b9c-bf19-3751ce4e6a1d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"535b99f0-d38e-4b9c-bf19-3751ce4e6a1d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"535b99f0-d38e-4b9c-bf19-3751ce4e6a1d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"535b99f0-d38e-4b9c-bf19-3751ce4e6a1d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"535b99f0-d38e-4b9c-bf19-3751ce4e6a1d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"nikolay.nikolov@jiminny.com\"} {\"correlation_id\":\"535b99f0-d38e-4b9c-bf19-3751ce4e6a1d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"nikolay.nikolov@jiminny.com\"} {\"correlation_id\":\"535b99f0-d38e-4b9c-bf19-3751ce4e6a1d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"nikolay.nikolov@jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"535b99f0-d38e-4b9c-bf19-3751ce4e6a1d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":614381,\"team_id\":2,\"email\":\"nikolay.nikolov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"535b99f0-d38e-4b9c-bf19-3751ce4e6a1d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":614381,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"535b99f0-d38e-4b9c-bf19-3751ce4e6a1d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":614381} {\"correlation_id\":\"535b99f0-d38e-4b9c-bf19-3751ce4e6a1d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":614381,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"535b99f0-d38e-4b9c-bf19-3751ce4e6a1d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":614381,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"535b99f0-d38e-4b9c-bf19-3751ce4e6a1d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":614378,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":6167,\"account_id\":null,\"opportunity_id\":null,\"stage_id\":null}} {\"correlation_id\":\"f934b469-16df-4a4b-b5cc-bf918da87b87\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":614378} {\"correlation_id\":\"f934b469-16df-4a4b-b5cc-bf918da87b87\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":614378,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"f934b469-16df-4a4b-b5cc-bf918da87b87\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":614378,\"participants\":[{\"id\":1002623,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":1002624,\"user_id\":null,\"contact_id\":6167,\"lead_id\":null},{\"id\":1002625,\"user_id\":89,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"f934b469-16df-4a4b-b5cc-bf918da87b87\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"f934b469-16df-4a4b-b5cc-bf918da87b87\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"f934b469-16df-4a4b-b5cc-bf918da87b87\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"f934b469-16df-4a4b-b5cc-bf918da87b87\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"f934b469-16df-4a4b-b5cc-bf918da87b87\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"nikolay.nikolov@jiminny.com\"} {\"correlation_id\":\"f934b469-16df-4a4b-b5cc-bf918da87b87\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"nikolay.nikolov@jiminny.com\"} {\"correlation_id\":\"f934b469-16df-4a4b-b5cc-bf918da87b87\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"nikolay.nikolov@jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"f934b469-16df-4a4b-b5cc-bf918da87b87\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"nmalchev@gmail.com\"} {\"correlation_id\":\"f934b469-16df-4a4b-b5cc-bf918da87b87\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":614378,\"team_id\":2,\"email\":\"nikolay.nikolov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"f934b469-16df-4a4b-b5cc-bf918da87b87\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":614378,\"participants_processed\":3,\"exact_matches\":1,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"f934b469-16df-4a4b-b5cc-bf918da87b87\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":614378} {\"correlation_id\":\"f934b469-16df-4a4b-b5cc-bf918da87b87\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":614378,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"f934b469-16df-4a4b-b5cc-bf918da87b87\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":614378,\"remote_search\":true,\"lead_id\":null,\"contact_id\":6167,\"account_id\":null,\"opportunity_id\":null,\"stage_id\":null} {\"correlation_id\":\"f934b469-16df-4a4b-b5cc-bf918da87b87\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":613840,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36}} {\"correlation_id\":\"b440c986-63bd-445c-adfc-4d111801e63c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613840} {\"correlation_id\":\"b440c986-63bd-445c-adfc-4d111801e63c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613840,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"b440c986-63bd-445c-adfc-4d111801e63c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":613840,\"participants\":[{\"id\":1001764,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null},{\"id\":1001765,\"user_id\":261,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"b440c986-63bd-445c-adfc-4d111801e63c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"b440c986-63bd-445c-adfc-4d111801e63c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"b440c986-63bd-445c-adfc-4d111801e63c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"b440c986-63bd-445c-adfc-4d111801e63c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"b440c986-63bd-445c-adfc-4d111801e63c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: ProspectCache - Searching DB for opportunity by owner {\"account_id\":244,\"contact_id\":4487,\"owner_id\":261} {\"correlation_id\":\"b440c986-63bd-445c-adfc-4d111801e63c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: ProspectCache - Opportunity DB search results {\"account_id\":244,\"contact_id\":4487,\"opportunity_id\":299} {\"correlation_id\":\"b440c986-63bd-445c-adfc-4d111801e63c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"b440c986-63bd-445c-adfc-4d111801e63c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":613840,\"team_id\":2,\"email\":\"adelina.petrova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"b440c986-63bd-445c-adfc-4d111801e63c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":613840,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"b440c986-63bd-445c-adfc-4d111801e63c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613840} {\"correlation_id\":\"b440c986-63bd-445c-adfc-4d111801e63c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613840,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"b440c986-63bd-445c-adfc-4d111801e63c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":613840,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36} {\"correlation_id\":\"b440c986-63bd-445c-adfc-4d111801e63c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":613833,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36}} {\"correlation_id\":\"0360a22a-94dd-43cb-b57b-06bc159a05ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613833} {\"correlation_id\":\"0360a22a-94dd-43cb-b57b-06bc159a05ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613833,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"0360a22a-94dd-43cb-b57b-06bc159a05ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":613833,\"participants\":[{\"id\":1001750,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null},{\"id\":1001751,\"user_id\":261,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"0360a22a-94dd-43cb-b57b-06bc159a05ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"0360a22a-94dd-43cb-b57b-06bc159a05ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"0360a22a-94dd-43cb-b57b-06bc159a05ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"0360a22a-94dd-43cb-b57b-06bc159a05ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"0360a22a-94dd-43cb-b57b-06bc159a05ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"0360a22a-94dd-43cb-b57b-06bc159a05ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":613833,\"team_id\":2,\"email\":\"adelina.petrova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"0360a22a-94dd-43cb-b57b-06bc159a05ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":613833,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"0360a22a-94dd-43cb-b57b-06bc159a05ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613833} {\"correlation_id\":\"0360a22a-94dd-43cb-b57b-06bc159a05ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613833,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"0360a22a-94dd-43cb-b57b-06bc159a05ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":613833,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36} {\"correlation_id\":\"0360a22a-94dd-43cb-b57b-06bc159a05ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":613827,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36}} {\"correlation_id\":\"32fbe130-bf29-4e37-9c5a-bbe81f416a36\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613827} {\"correlation_id\":\"32fbe130-bf29-4e37-9c5a-bbe81f416a36\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613827,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"32fbe130-bf29-4e37-9c5a-bbe81f416a36\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":613827,\"participants\":[{\"id\":1001734,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null},{\"id\":1001735,\"user_id\":261,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"32fbe130-bf29-4e37-9c5a-bbe81f416a36\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"32fbe130-bf29-4e37-9c5a-bbe81f416a36\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"32fbe130-bf29-4e37-9c5a-bbe81f416a36\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"32fbe130-bf29-4e37-9c5a-bbe81f416a36\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"32fbe130-bf29-4e37-9c5a-bbe81f416a36\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"32fbe130-bf29-4e37-9c5a-bbe81f416a36\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":613827,\"team_id\":2,\"email\":\"adelina.petrova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"32fbe130-bf29-4e37-9c5a-bbe81f416a36\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":613827,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"32fbe130-bf29-4e37-9c5a-bbe81f416a36\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613827} {\"correlation_id\":\"32fbe130-bf29-4e37-9c5a-bbe81f416a36\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613827,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"32fbe130-bf29-4e37-9c5a-bbe81f416a36\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":613827,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36} {\"correlation_id\":\"32fbe130-bf29-4e37-9c5a-bbe81f416a36\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":613826,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36}} {\"correlation_id\":\"9b9716ff-ede9-4377-8ee8-b3c9a68b2dfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613826} {\"correlation_id\":\"9b9716ff-ede9-4377-8ee8-b3c9a68b2dfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613826,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"9b9716ff-ede9-4377-8ee8-b3c9a68b2dfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":613826,\"participants\":[{\"id\":1001732,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null},{\"id\":1001733,\"user_id\":261,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"9b9716ff-ede9-4377-8ee8-b3c9a68b2dfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"9b9716ff-ede9-4377-8ee8-b3c9a68b2dfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"9b9716ff-ede9-4377-8ee8-b3c9a68b2dfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"9b9716ff-ede9-4377-8ee8-b3c9a68b2dfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"9b9716ff-ede9-4377-8ee8-b3c9a68b2dfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"9b9716ff-ede9-4377-8ee8-b3c9a68b2dfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":613826,\"team_id\":2,\"email\":\"adelina.petrova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"9b9716ff-ede9-4377-8ee8-b3c9a68b2dfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":613826,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"9b9716ff-ede9-4377-8ee8-b3c9a68b2dfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613826} {\"correlation_id\":\"9b9716ff-ede9-4377-8ee8-b3c9a68b2dfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613826,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"9b9716ff-ede9-4377-8ee8-b3c9a68b2dfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":613826,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36} {\"correlation_id\":\"9b9716ff-ede9-4377-8ee8-b3c9a68b2dfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":613820,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36}} {\"correlation_id\":\"a131075c-6ae7-4912-92e1-adc33c291d1e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613820} {\"correlation_id\":\"a131075c-6ae7-4912-92e1-adc33c291d1e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613820,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"a131075c-6ae7-4912-92e1-adc33c291d1e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":613820,\"participants\":[{\"id\":1001721,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null},{\"id\":1001722,\"user_id\":261,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"a131075c-6ae7-4912-92e1-adc33c291d1e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"a131075c-6ae7-4912-92e1-adc33c291d1e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"a131075c-6ae7-4912-92e1-adc33c291d1e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"a131075c-6ae7-4912-92e1-adc33c291d1e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"a131075c-6ae7-4912-92e1-adc33c291d1e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"a131075c-6ae7-4912-92e1-adc33c291d1e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":613820,\"team_id\":2,\"email\":\"adelina.petrova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"a131075c-6ae7-4912-92e1-adc33c291d1e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":613820,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"a131075c-6ae7-4912-92e1-adc33c291d1e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613820} {\"correlation_id\":\"a131075c-6ae7-4912-92e1-adc33c291d1e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613820,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"a131075c-6ae7-4912-92e1-adc33c291d1e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":613820,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36} {\"correlation_id\":\"a131075c-6ae7-4912-92e1-adc33c291d1e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":613818,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36}} {\"correlation_id\":\"d36e09b8-5b15-40f2-8fa0-0beb3f0ebe69\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613818} {\"correlation_id\":\"d36e09b8-5b15-40f2-8fa0-0beb3f0ebe69\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613818,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"d36e09b8-5b15-40f2-8fa0-0beb3f0ebe69\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":613818,\"participants\":[{\"id\":1001717,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null},{\"id\":1001718,\"user_id\":261,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"d36e09b8-5b15-40f2-8fa0-0beb3f0ebe69\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"d36e09b8-5b15-40f2-8fa0-0beb3f0ebe69\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"d36e09b8-5b15-40f2-8fa0-0beb3f0ebe69\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d36e09b8-5b15-40f2-8fa0-0beb3f0ebe69\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"d36e09b8-5b15-40f2-8fa0-0beb3f0ebe69\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"d36e09b8-5b15-40f2-8fa0-0beb3f0ebe69\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":613818,\"team_id\":2,\"email\":\"adelina.petrova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"d36e09b8-5b15-40f2-8fa0-0beb3f0ebe69\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":613818,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"d36e09b8-5b15-40f2-8fa0-0beb3f0ebe69\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613818} {\"correlation_id\":\"d36e09b8-5b15-40f2-8fa0-0beb3f0ebe69\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613818,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"d36e09b8-5b15-40f2-8fa0-0beb3f0ebe69\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":613818,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36} {\"correlation_id\":\"d36e09b8-5b15-40f2-8fa0-0beb3f0ebe69\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":613812,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36}} {\"correlation_id\":\"9e5ea20b-def9-4ace-8a73-9d08a04cc6d9\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613812} {\"correlation_id\":\"9e5ea20b-def9-4ace-8a73-9d08a04cc6d9\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613812,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"9e5ea20b-def9-4ace-8a73-9d08a04cc6d9\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":613812,\"participants\":[{\"id\":1001705,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null},{\"id\":1001706,\"user_id\":261,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"9e5ea20b-def9-4ace-8a73-9d08a04cc6d9\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"9e5ea20b-def9-4ace-8a73-9d08a04cc6d9\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"9e5ea20b-def9-4ace-8a73-9d08a04cc6d9\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"9e5ea20b-def9-4ace-8a73-9d08a04cc6d9\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"9e5ea20b-def9-4ace-8a73-9d08a04cc6d9\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"9e5ea20b-def9-4ace-8a73-9d08a04cc6d9\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":613812,\"team_id\":2,\"email\":\"adelina.petrova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"9e5ea20b-def9-4ace-8a73-9d08a04cc6d9\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":613812,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"9e5ea20b-def9-4ace-8a73-9d08a04cc6d9\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613812} {\"correlation_id\":\"9e5ea20b-def9-4ace-8a73-9d08a04cc6d9\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613812,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"9e5ea20b-def9-4ace-8a73-9d08a04cc6d9\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":613812,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36} {\"correlation_id\":\"9e5ea20b-def9-4ace-8a73-9d08a04cc6d9\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":613807,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4484,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"be53e876-a319-40dc-a89b-0728d9a47c6e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613807} {\"correlation_id\":\"be53e876-a319-40dc-a89b-0728d9a47c6e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613807,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"be53e876-a319-40dc-a89b-0728d9a47c6e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":613807,\"participants\":[{\"id\":1001690,\"user_id\":253,\"contact_id\":null,\"lead_id\":null},{\"id\":1001691,\"user_id\":null,\"contact_id\":4484,\"lead_id\":null}]} {\"correlation_id\":\"be53e876-a319-40dc-a89b-0728d9a47c6e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"be53e876-a319-40dc-a89b-0728d9a47c6e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"be53e876-a319-40dc-a89b-0728d9a47c6e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"be53e876-a319-40dc-a89b-0728d9a47c6e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"be53e876-a319-40dc-a89b-0728d9a47c6e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":613807,\"team_id\":2,\"email\":\"preslava.ivanova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"be53e876-a319-40dc-a89b-0728d9a47c6e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: ProspectCache - Searching DB for opportunity by owner {\"account_id\":243,\"contact_id\":4484,\"owner_id\":253} {\"correlation_id\":\"be53e876-a319-40dc-a89b-0728d9a47c6e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: ProspectCache - Fallback DB opportunity search {\"account_id\":243,\"contact_id\":4484} {\"correlation_id\":\"be53e876-a319-40dc-a89b-0728d9a47c6e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: ProspectCache - Opportunity DB search results {\"account_id\":243,\"contact_id\":4484,\"opportunity_id\":276} {\"correlation_id\":\"be53e876-a319-40dc-a89b-0728d9a47c6e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"tsvetomir.banovski@gmail.com\"} {\"correlation_id\":\"be53e876-a319-40dc-a89b-0728d9a47c6e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":613807,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"be53e876-a319-40dc-a89b-0728d9a47c6e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613807} {\"correlation_id\":\"be53e876-a319-40dc-a89b-0728d9a47c6e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613807,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"be53e876-a319-40dc-a89b-0728d9a47c6e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":613807,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4484,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"be53e876-a319-40dc-a89b-0728d9a47c6e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":613806,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":350,\"stage_id\":34}} {\"correlation_id\":\"94c53200-4779-41ab-82ea-571c1bcdd8b7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613806} {\"correlation_id\":\"94c53200-4779-41ab-82ea-571c1bcdd8b7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613806,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"94c53200-4779-41ab-82ea-571c1bcdd8b7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":613806,\"participants\":[{\"id\":1001688,\"user_id\":253,\"contact_id\":null,\"lead_id\":null},{\"id\":1001689,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null}]} {\"correlation_id\":\"94c53200-4779-41ab-82ea-571c1bcdd8b7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"94c53200-4779-41ab-82ea-571c1bcdd8b7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"94c53200-4779-41ab-82ea-571c1bcdd8b7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"94c53200-4779-41ab-82ea-571c1bcdd8b7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"94c53200-4779-41ab-82ea-571c1bcdd8b7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":613806,\"team_id\":2,\"email\":\"preslava.ivanova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"94c53200-4779-41ab-82ea-571c1bcdd8b7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: ProspectCache - Searching DB for opportunity by owner {\"account_id\":244,\"contact_id\":4487,\"owner_id\":253} {\"correlation_id\":\"94c53200-4779-41ab-82ea-571c1bcdd8b7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: ProspectCache - Fallback DB opportunity search {\"account_id\":244,\"contact_id\":4487} {\"correlation_id\":\"94c53200-4779-41ab-82ea-571c1bcdd8b7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: ProspectCache - Opportunity DB search results {\"account_id\":244,\"contact_id\":4487,\"opportunity_id\":350} {\"correlation_id\":\"94c53200-4779-41ab-82ea-571c1bcdd8b7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"94c53200-4779-41ab-82ea-571c1bcdd8b7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":613806,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"94c53200-4779-41ab-82ea-571c1bcdd8b7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613806} {\"correlation_id\":\"94c53200-4779-41ab-82ea-571c1bcdd8b7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613806,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"94c53200-4779-41ab-82ea-571c1bcdd8b7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":613806,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":350,\"stage_id\":34} {\"correlation_id\":\"94c53200-4779-41ab-82ea-571c1bcdd8b7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":613805,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":350,\"stage_id\":34}} {\"correlation_id\":\"d1a190ef-9b03-49ff-ad47-50afa53ef345\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613805} {\"correlation_id\":\"d1a190ef-9b03-49ff-ad47-50afa53ef345\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613805,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"d1a190ef-9b03-49ff-ad47-50afa53ef345\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":613805,\"participants\":[{\"id\":1001686,\"user_id\":253,\"contact_id\":null,\"lead_id\":null},{\"id\":1001687,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null}]} {\"correlation_id\":\"d1a190ef-9b03-49ff-ad47-50afa53ef345\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"d1a190ef-9b03-49ff-ad47-50afa53ef345\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"d1a190ef-9b03-49ff-ad47-50afa53ef345\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d1a190ef-9b03-49ff-ad47-50afa53ef345\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"d1a190ef-9b03-49ff-ad47-50afa53ef345\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":613805,\"team_id\":2,\"email\":\"preslava.ivanova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"d1a190ef-9b03-49ff-ad47-50afa53ef345\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"d1a190ef-9b03-49ff-ad47-50afa53ef345\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":613805,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"d1a190ef-9b03-49ff-ad47-50afa53ef345\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613805} {\"correlation_id\":\"d1a190ef-9b03-49ff-ad47-50afa53ef345\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613805,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"d1a190ef-9b03-49ff-ad47-50afa53ef345\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":613805,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":350,\"stage_id\":34} {\"correlation_id\":\"d1a190ef-9b03-49ff-ad47-50afa53ef345\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":613698,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36}} {\"correlation_id\":\"2fa5fec1-54fb-40cc-93d4-145712a9d2fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613698} {\"correlation_id\":\"2fa5fec1-54fb-40cc-93d4-145712a9d2fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613698,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"2fa5fec1-54fb-40cc-93d4-145712a9d2fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":613698,\"participants\":[{\"id\":1001667,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null},{\"id\":1001668,\"user_id\":261,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"2fa5fec1-54fb-40cc-93d4-145712a9d2fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"2fa5fec1-54fb-40cc-93d4-145712a9d2fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"2fa5fec1-54fb-40cc-93d4-145712a9d2fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"2fa5fec1-54fb-40cc-93d4-145712a9d2fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"2fa5fec1-54fb-40cc-93d4-145712a9d2fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"2fa5fec1-54fb-40cc-93d4-145712a9d2fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":613698,\"team_id\":2,\"email\":\"adelina.petrova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"2fa5fec1-54fb-40cc-93d4-145712a9d2fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":613698,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"2fa5fec1-54fb-40cc-93d4-145712a9d2fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613698} {\"correlation_id\":\"2fa5fec1-54fb-40cc-93d4-145712a9d2fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613698,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"2fa5fec1-54fb-40cc-93d4-145712a9d2fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":613698,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36} {\"correlation_id\":\"2fa5fec1-54fb-40cc-93d4-145712a9d2fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":613697,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36}} {\"correlation_id\":\"4da249c2-1056-47f9-869a-53c5fda5b9ef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613697} {\"correlation_id\":\"4da249c2-1056-47f9-869a-53c5fda5b9ef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613697,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"4da249c2-1056-47f9-869a-53c5fda5b9ef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":613697,\"participants\":[{\"id\":1001665,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null},{\"id\":1001666,\"user_id\":261,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"4da249c2-1056-47f9-869a-53c5fda5b9ef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"4da249c2-1056-47f9-869a-53c5fda5b9ef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"4da249c2-1056-47f9-869a-53c5fda5b9ef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4da249c2-1056-47f9-869a-53c5fda5b9ef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"4da249c2-1056-47f9-869a-53c5fda5b9ef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"4da249c2-1056-47f9-869a-53c5fda5b9ef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":613697,\"team_id\":2,\"email\":\"adelina.petrova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"4da249c2-1056-47f9-869a-53c5fda5b9ef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":613697,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"4da249c2-1056-47f9-869a-53c5fda5b9ef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613697} {\"correlation_id\":\"4da249c2-1056-47f9-869a-53c5fda5b9ef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613697,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"4da249c2-1056-47f9-869a-53c5fda5b9ef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":613697,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36} {\"correlation_id\":\"4da249c2-1056-47f9-869a-53c5fda5b9ef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":613696,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36}} {\"correlation_id\":\"8224abca-8e96-4a39-8baa-b4bc4f746e1b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613696} {\"correlation_id\":\"8224abca-8e96-4a39-8baa-b4bc4f746e1b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613696,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"8224abca-8e96-4a39-8baa-b4bc4f746e1b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":613696,\"participants\":[{\"id\":1001663,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null},{\"id\":1001664,\"user_id\":261,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"8224abca-8e96-4a39-8baa-b4bc4f746e1b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"8224abca-8e96-4a39-8baa-b4bc4f746e1b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"8224abca-8e96-4a39-8baa-b4bc4f746e1b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"8224abca-8e96-4a39-8baa-b4bc4f746e1b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"8224abca-8e96-4a39-8baa-b4bc4f746e1b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"8224abca-8e96-4a39-8baa-b4bc4f746e1b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":613696,\"team_id\":2,\"email\":\"adelina.petrova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"8224abca-8e96-4a39-8baa-b4bc4f746e1b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":613696,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"8224abca-8e96-4a39-8baa-b4bc4f746e1b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613696} {\"correlation_id\":\"8224abca-8e96-4a39-8baa-b4bc4f746e1b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613696,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"8224abca-8e96-4a39-8baa-b4bc4f746e1b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":613696,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36} {\"correlation_id\":\"8224abca-8e96-4a39-8baa-b4bc4f746e1b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":613695,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36}} {\"correlation_id\":\"e6c53892-ee7f-42df-ae80-06e4cab7eeea\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613695} {\"correlation_id\":\"e6c53892-ee7f-42df-ae80-06e4cab7eeea\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613695,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"e6c53892-ee7f-42df-ae80-06e4cab7eeea\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":613695,\"participants\":[{\"id\":1001661,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null},{\"id\":1001662,\"user_id\":261,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"e6c53892-ee7f-42df-ae80-06e4cab7eeea\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"e6c53892-ee7f-42df-ae80-06e4cab7eeea\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"e6c53892-ee7f-42df-ae80-06e4cab7eeea\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"e6c53892-ee7f-42df-ae80-06e4cab7eeea\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"e6c53892-ee7f-42df-ae80-06e4cab7eeea\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"e6c53892-ee7f-42df-ae80-06e4cab7eeea\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":613695,\"team_id\":2,\"email\":\"adelina.petrova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"e6c53892-ee7f-42df-ae80-06e4cab7eeea\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":613695,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"e6c53892-ee7f-42df-ae80-06e4cab7eeea\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613695} {\"correlation_id\":\"e6c53892-ee7f-42df-ae80-06e4cab7eeea\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613695,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"e6c53892-ee7f-42df-ae80-06e4cab7eeea\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":613695,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36} {\"correlation_id\":\"e6c53892-ee7f-42df-ae80-06e4cab7eeea\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.NOTICE: Monitoring start {\"correlation_id\":\"2766b9f0-0c60-4d94-9b69-4f2dc977fe59\",\"trace_id\":\"0810b004-42ab-49a4-b3bf-4fbda4a819fe\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":613694,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36}} {\"correlation_id\":\"34f7f8f5-b1b3-4176-bc4a-f96265db81c5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613694} {\"correlation_id\":\"34f7f8f5-b1b3-4176-bc4a-f96265db81c5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613694,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"34f7f8f5-b1b3-4176-bc4a-f96265db81c5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.NOTICE: Monitoring end {\"correlation_id\":\"2766b9f0-0c60-4d94-9b69-4f2dc977fe59\",\"trace_id\":\"0810b004-42ab-49a4-b3bf-4fbda4a819fe\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":613694,\"participants\":[{\"id\":1001659,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null},{\"id\":1001660,\"user_id\":261,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"34f7f8f5-b1b3-4176-bc4a-f96265db81c5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"34f7f8f5-b1b3-4176-bc4a-f96265db81c5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"34f7f8f5-b1b3-4176-bc4a-f96265db81c5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"34f7f8f5-b1b3-4176-bc4a-f96265db81c5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"34f7f8f5-b1b3-4176-bc4a-f96265db81c5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"34f7f8f5-b1b3-4176-bc4a-f96265db81c5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":613694,\"team_id\":2,\"email\":\"adelina.petrova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"34f7f8f5-b1b3-4176-bc4a-f96265db81c5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":613694,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"34f7f8f5-b1b3-4176-bc4a-f96265db81c5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613694} {\"correlation_id\":\"34f7f8f5-b1b3-4176-bc4a-f96265db81c5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613694,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"34f7f8f5-b1b3-4176-bc4a-f96265db81c5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":613694,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36} {\"correlation_id\":\"34f7f8f5-b1b3-4176-bc4a-f96265db81c5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":613157,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":350,\"stage_id\":34}} {\"correlation_id\":\"38fd2794-89d3-4ce9-a9fc-f9f8382e30d5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613157} {\"correlation_id\":\"38fd2794-89d3-4ce9-a9fc-f9f8382e30d5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613157,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"38fd2794-89d3-4ce9-a9fc-f9f8382e30d5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":613157,\"participants\":[{\"id\":1000746,\"user_id\":253,\"contact_id\":null,\"lead_id\":null},{\"id\":1000747,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null}]} {\"correlation_id\":\"38fd2794-89d3-4ce9-a9fc-f9f8382e30d5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"38fd2794-89d3-4ce9-a9fc-f9f8382e30d5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"38fd2794-89d3-4ce9-a9fc-f9f8382e30d5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"38fd2794-89d3-4ce9-a9fc-f9f8382e30d5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"38fd2794-89d3-4ce9-a9fc-f9f8382e30d5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":613157,\"team_id\":2,\"email\":\"preslava.ivanova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"38fd2794-89d3-4ce9-a9fc-f9f8382e30d5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"38fd2794-89d3-4ce9-a9fc-f9f8382e30d5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":613157,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"38fd2794-89d3-4ce9-a9fc-f9f8382e30d5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613157} {\"correlation_id\":\"38fd2794-89d3-4ce9-a9fc-f9f8382e30d5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613157,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"38fd2794-89d3-4ce9-a9fc-f9f8382e30d5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":613157,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":350,\"stage_id\":34} {\"correlation_id\":\"38fd2794-89d3-4ce9-a9fc-f9f8382e30d5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":613156,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":350,\"stage_id\":34}} {\"correlation_id\":\"f33d171a-96c9-465c-bcf2-facecea0a7d6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613156} {\"correlation_id\":\"f33d171a-96c9-465c-bcf2-facecea0a7d6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613156,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"f33d171a-96c9-465c-bcf2-facecea0a7d6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":613156,\"participants\":[{\"id\":1000744,\"user_id\":253,\"contact_id\":null,\"lead_id\":null},{\"id\":1000745,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null}]} {\"correlation_id\":\"f33d171a-96c9-465c-bcf2-facecea0a7d6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"f33d171a-96c9-465c-bcf2-facecea0a7d6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"f33d171a-96c9-465c-bcf2-facecea0a7d6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"f33d171a-96c9-465c-bcf2-facecea0a7d6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"f33d171a-96c9-465c-bcf2-facecea0a7d6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":613156,\"team_id\":2,\"email\":\"preslava.ivanova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"f33d171a-96c9-465c-bcf2-facecea0a7d6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"f33d171a-96c9-465c-bcf2-facecea0a7d6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":613156,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"f33d171a-96c9-465c-bcf2-facecea0a7d6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613156} {\"correlation_id\":\"f33d171a-96c9-465c-bcf2-facecea0a7d6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613156,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"f33d171a-96c9-465c-bcf2-facecea0a7d6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":613156,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":350,\"stage_id\":34} {\"correlation_id\":\"f33d171a-96c9-465c-bcf2-facecea0a7d6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":613155,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":350,\"stage_id\":34}} {\"correlation_id\":\"6907203d-5426-446b-b0ed-cb3a79828876\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613155} {\"correlation_id\":\"6907203d-5426-446b-b0ed-cb3a79828876\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613155,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"6907203d-5426-446b-b0ed-cb3a79828876\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":613155,\"participants\":[{\"id\":1000742,\"user_id\":253,\"contact_id\":null,\"lead_id\":null},{\"id\":1000743,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null}]} {\"correlation_id\":\"6907203d-5426-446b-b0ed-cb3a79828876\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"6907203d-5426-446b-b0ed-cb3a79828876\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"6907203d-5426-446b-b0ed-cb3a79828876\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"6907203d-5426-446b-b0ed-cb3a79828876\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"6907203d-5426-446b-b0ed-cb3a79828876\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":613155,\"team_id\":2,\"email\":\"preslava.ivanova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"6907203d-5426-446b-b0ed-cb3a79828876\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"6907203d-5426-446b-b0ed-cb3a79828876\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":613155,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"6907203d-5426-446b-b0ed-cb3a79828876\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613155} {\"correlation_id\":\"6907203d-5426-446b-b0ed-cb3a79828876\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613155,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"6907203d-5426-446b-b0ed-cb3a79828876\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":613155,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":350,\"stage_id\":34} {\"correlation_id\":\"6907203d-5426-446b-b0ed-cb3a79828876\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":613130,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36}} {\"correlation_id\":\"b39eb37f-ecba-45cf-99d3-dc45ee004976\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613130} {\"correlation_id\":\"b39eb37f-ecba-45cf-99d3-dc45ee004976\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613130,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"b39eb37f-ecba-45cf-99d3-dc45ee004976\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":613130,\"participants\":[{\"id\":1000693,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null},{\"id\":1000694,\"user_id\":261,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"b39eb37f-ecba-45cf-99d3-dc45ee004976\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"b39eb37f-ecba-45cf-99d3-dc45ee004976\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"b39eb37f-ecba-45cf-99d3-dc45ee004976\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"b39eb37f-ecba-45cf-99d3-dc45ee004976\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"b39eb37f-ecba-45cf-99d3-dc45ee004976\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"b39eb37f-ecba-45cf-99d3-dc45ee004976\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":613130,\"team_id\":2,\"email\":\"adelina.petrova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"b39eb37f-ecba-45cf-99d3-dc45ee004976\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":613130,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"b39eb37f-ecba-45cf-99d3-dc45ee004976\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613130} {\"correlation_id\":\"b39eb37f-ecba-45cf-99d3-dc45ee004976\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613130,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"b39eb37f-ecba-45cf-99d3-dc45ee004976\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":613130,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36} {\"correlation_id\":\"b39eb37f-ecba-45cf-99d3-dc45ee004976\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612924,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":165,\"stage_id\":89}} {\"correlation_id\":\"22353f3a-e0aa-43eb-9a06-f618d38746e7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612924} {\"correlation_id\":\"22353f3a-e0aa-43eb-9a06-f618d38746e7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612924,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"22353f3a-e0aa-43eb-9a06-f618d38746e7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612924,\"participants\":[{\"id\":1000290,\"user_id\":19,\"contact_id\":null,\"lead_id\":null},{\"id\":1000291,\"user_id\":null,\"contact_id\":97,\"lead_id\":null}]} {\"correlation_id\":\"22353f3a-e0aa-43eb-9a06-f618d38746e7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"22353f3a-e0aa-43eb-9a06-f618d38746e7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"22353f3a-e0aa-43eb-9a06-f618d38746e7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"22353f3a-e0aa-43eb-9a06-f618d38746e7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"22353f3a-e0aa-43eb-9a06-f618d38746e7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612924,\"team_id\":2,\"email\":\"james.graham@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"22353f3a-e0aa-43eb-9a06-f618d38746e7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: ProspectCache - Searching DB for opportunity by owner {\"account_id\":69,\"contact_id\":97,\"owner_id\":19} {\"correlation_id\":\"22353f3a-e0aa-43eb-9a06-f618d38746e7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: ProspectCache - Opportunity DB search results {\"account_id\":69,\"contact_id\":97,\"opportunity_id\":165} {\"correlation_id\":\"22353f3a-e0aa-43eb-9a06-f618d38746e7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinsoncrusoe@test.com\"} {\"correlation_id\":\"22353f3a-e0aa-43eb-9a06-f618d38746e7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612924,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"22353f3a-e0aa-43eb-9a06-f618d38746e7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612924} {\"correlation_id\":\"22353f3a-e0aa-43eb-9a06-f618d38746e7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612924,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"22353f3a-e0aa-43eb-9a06-f618d38746e7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612924,\"remote_search\":true,\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":165,\"stage_id\":89} {\"correlation_id\":\"22353f3a-e0aa-43eb-9a06-f618d38746e7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612923,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":165,\"stage_id\":89}} {\"correlation_id\":\"602cc3c5-932c-4d3d-9370-35217adac6e0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612923} {\"correlation_id\":\"602cc3c5-932c-4d3d-9370-35217adac6e0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612923,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"602cc3c5-932c-4d3d-9370-35217adac6e0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612923,\"participants\":[{\"id\":1000288,\"user_id\":19,\"contact_id\":null,\"lead_id\":null},{\"id\":1000289,\"user_id\":null,\"contact_id\":97,\"lead_id\":null}]} {\"correlation_id\":\"602cc3c5-932c-4d3d-9370-35217adac6e0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"602cc3c5-932c-4d3d-9370-35217adac6e0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"602cc3c5-932c-4d3d-9370-35217adac6e0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"602cc3c5-932c-4d3d-9370-35217adac6e0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"602cc3c5-932c-4d3d-9370-35217adac6e0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612923,\"team_id\":2,\"email\":\"james.graham@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"602cc3c5-932c-4d3d-9370-35217adac6e0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinsoncrusoe@test.com\"} {\"correlation_id\":\"602cc3c5-932c-4d3d-9370-35217adac6e0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612923,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"602cc3c5-932c-4d3d-9370-35217adac6e0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612923} {\"correlation_id\":\"602cc3c5-932c-4d3d-9370-35217adac6e0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612923,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"602cc3c5-932c-4d3d-9370-35217adac6e0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612923,\"remote_search\":true,\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":165,\"stage_id\":89} {\"correlation_id\":\"602cc3c5-932c-4d3d-9370-35217adac6e0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612922,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":165,\"stage_id\":89}} {\"correlation_id\":\"4272a600-7709-4e0b-9ddd-2bf9a93ee051\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612922} {\"correlation_id\":\"4272a600-7709-4e0b-9ddd-2bf9a93ee051\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612922,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"4272a600-7709-4e0b-9ddd-2bf9a93ee051\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612922,\"participants\":[{\"id\":1000286,\"user_id\":19,\"contact_id\":null,\"lead_id\":null},{\"id\":1000287,\"user_id\":null,\"contact_id\":97,\"lead_id\":null}]} {\"correlation_id\":\"4272a600-7709-4e0b-9ddd-2bf9a93ee051\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"4272a600-7709-4e0b-9ddd-2bf9a93ee051\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"4272a600-7709-4e0b-9ddd-2bf9a93ee051\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4272a600-7709-4e0b-9ddd-2bf9a93ee051\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"4272a600-7709-4e0b-9ddd-2bf9a93ee051\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612922,\"team_id\":2,\"email\":\"james.graham@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"4272a600-7709-4e0b-9ddd-2bf9a93ee051\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinsoncrusoe@test.com\"} {\"correlation_id\":\"4272a600-7709-4e0b-9ddd-2bf9a93ee051\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612922,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"4272a600-7709-4e0b-9ddd-2bf9a93ee051\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612922} {\"correlation_id\":\"4272a600-7709-4e0b-9ddd-2bf9a93ee051\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612922,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"4272a600-7709-4e0b-9ddd-2bf9a93ee051\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612922,\"remote_search\":true,\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":165,\"stage_id\":89} {\"correlation_id\":\"4272a600-7709-4e0b-9ddd-2bf9a93ee051\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612847,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36}} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612847} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612847,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612847,\"participants\":[{\"id\":1000130,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":1000131,\"user_id\":261,\"contact_id\":null,\"lead_id\":null},{\"id\":1000151,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null}]} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [Prospect match] Cache miss, calling the API {\"identifier_type\":\"email\",\"identifier\":\"adelina.petrova@jiminny.com\"} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [Hubspot] Failed to fetch contact {\"email\":\"adelina.petrova@jiminny.com\",\"reason\":\"[404] Client error: `GET https://api.hubapi.com/crm/v3/objects/contacts/adelina.petrova%40jiminny.com?properties=email%2Cfirstname%2Clastname%2Ccountry%2Cphone%2Cmobilephone%2Cjobtitle%2Chubspot_owner_id%2Cassociatedcompanyid%2Cphoto&archived=0&idProperty=email` resulted in a `404 Not Found` response\"} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [Prospect match] API returned empty result, caching the miss with empty prospect data {\"identifier_type\":\"email\",\"identifier\":\"adelina.petrova@jiminny.com\"} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:27] local.INFO: [Hubspot] Pagination completed {\"team_id\":2,\"endpoint\":\"https://api.hubapi.com/crm/v3/objects/contact/search\",\"total_requests\":1,\"total_records_fetched\":0,\"total_elapsed_seconds\":0.22,\"average_seconds_per_request\":0.22} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:27] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"adelina.petrova@jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:27] local.INFO: [Prospect match] Cache miss {\"identifier_type\":\"domain\",\"identifier\":\"jiminny.com\",\"crm\":\"hubspot\"} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:27] local.INFO: [Prospect match] Cache miss, calling the API {\"identifier_type\":\"domain\",\"identifier\":\"jiminny.com\"} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:27] local.INFO: [HubSpot] importAccount {\"crm_provider_id\":\"749766179\",\"config_id\":2} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:27] local.INFO: [HubSpot] CRM Search requested {\"request\":{\"filterGroups\":[{\"filters\":[{\"propertyName\":\"associations.company\",\"operator\":\"EQ\",\"value\":\"749766179\"},{\"propertyName\":\"dealstage\",\"operator\":\"NOT_IN\",\"values\":[\"closedwon\",\"4040964\",\"59247967\"]},{\"propertyName\":\"dealstage\",\"operator\":\"NOT_IN\",\"values\":[\"closedlost\",\"4040965\",\"59247968\"]}]}],\"sorts\":[{\"propertyName\":\"modifieddate\",\"direction\":\"DESCENDING\"}],\"properties\":[\"dealname\",\"amount\",\"hubspot_owner_id\",\"pipeline\",\"dealstage\",\"closedate\",\"deal_currency_code\"],\"limit\":200}} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:27] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"6bdeb8f0-c361-4cb3-9fb1-c6bdf466a73e\",\"trace_id\":\"d050352e-9bf5-497a-99d6-e46661a04a04\"}\n[2026-05-11 10:43:27] local.INFO: [Hubspot] Pagination completed {\"team_id\":2,\"endpoint\":\"https://api.hubapi.com/crm/v3/objects/deals/search\",\"total_requests\":1,\"total_records_fetched\":10,\"total_elapsed_seconds\":0.25,\"average_seconds_per_request\":0.25} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:27] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":60.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"6bdeb8f0-c361-4cb3-9fb1-c6bdf466a73e\",\"trace_id\":\"d050352e-9bf5-497a-99d6-e46661a04a04\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612847,\"team_id\":2,\"email\":\"adelina.petrova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612847,\"participants_processed\":3,\"exact_matches\":1,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612847} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612847,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612847,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612822,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36}} {\"correlation_id\":\"a8970854-ad6a-4751-9cc5-ffe651d20f06\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612822} {\"correlation_id\":\"a8970854-ad6a-4751-9cc5-ffe651d20f06\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612822,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"a8970854-ad6a-4751-9cc5-ffe651d20f06\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612822,\"participants\":[{\"id\":1000080,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null},{\"id\":1000081,\"user_id\":261,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"a8970854-ad6a-4751-9cc5-ffe651d20f06\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"a8970854-ad6a-4751-9cc5-ffe651d20f06\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"a8970854-ad6a-4751-9cc5-ffe651d20f06\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"a8970854-ad6a-4751-9cc5-ffe651d20f06\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"a8970854-ad6a-4751-9cc5-ffe651d20f06\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"a8970854-ad6a-4751-9cc5-ffe651d20f06\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612822,\"team_id\":2,\"email\":\"adelina.petrova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"a8970854-ad6a-4751-9cc5-ffe651d20f06\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612822,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"a8970854-ad6a-4751-9cc5-ffe651d20f06\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612822} {\"correlation_id\":\"a8970854-ad6a-4751-9cc5-ffe651d20f06\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612822,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"a8970854-ad6a-4751-9cc5-ffe651d20f06\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612822,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36} {\"correlation_id\":\"a8970854-ad6a-4751-9cc5-ffe651d20f06\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612819,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36}} {\"correlation_id\":\"16e6bc16-5ab8-49ce-b7da-c292b8c94105\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612819} {\"correlation_id\":\"16e6bc16-5ab8-49ce-b7da-c292b8c94105\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612819,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"16e6bc16-5ab8-49ce-b7da-c292b8c94105\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612819,\"participants\":[{\"id\":1000073,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null},{\"id\":1000074,\"user_id\":261,\"contact_id\":null,\"lead_id\":null},{\"id\":1000075,\"user_id\":null,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"16e6bc16-5ab8-49ce-b7da-c292b8c94105\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"16e6bc16-5ab8-49ce-b7da-c292b8c94105\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"16e6bc16-5ab8-49ce-b7da-c292b8c94105\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"16e6bc16-5ab8-49ce-b7da-c292b8c94105\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"16e6bc16-5ab8-49ce-b7da-c292b8c94105\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"16e6bc16-5ab8-49ce-b7da-c292b8c94105\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612819,\"team_id\":2,\"email\":\"adelina.petrova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"16e6bc16-5ab8-49ce-b7da-c292b8c94105\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"adelina.petrova@jiminny.com\"} {\"correlation_id\":\"16e6bc16-5ab8-49ce-b7da-c292b8c94105\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"adelina.petrova@jiminny.com\"} {\"correlation_id\":\"16e6bc16-5ab8-49ce-b7da-c292b8c94105\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"adelina.petrova@jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"16e6bc16-5ab8-49ce-b7da-c292b8c94105\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612819,\"participants_processed\":3,\"exact_matches\":1,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"16e6bc16-5ab8-49ce-b7da-c292b8c94105\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612819} {\"correlation_id\":\"16e6bc16-5ab8-49ce-b7da-c292b8c94105\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612819,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"16e6bc16-5ab8-49ce-b7da-c292b8c94105\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612819,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36} {\"correlation_id\":\"16e6bc16-5ab8-49ce-b7da-c292b8c94105\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612673,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36}} {\"correlation_id\":\"8711a017-62dd-479f-b84e-123d6e3f40be\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612673} {\"correlation_id\":\"8711a017-62dd-479f-b84e-123d6e3f40be\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612673,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"8711a017-62dd-479f-b84e-123d6e3f40be\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612673,\"participants\":[{\"id\":999993,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null},{\"id\":999994,\"user_id\":261,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"8711a017-62dd-479f-b84e-123d6e3f40be\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"8711a017-62dd-479f-b84e-123d6e3f40be\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"8711a017-62dd-479f-b84e-123d6e3f40be\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"8711a017-62dd-479f-b84e-123d6e3f40be\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"8711a017-62dd-479f-b84e-123d6e3f40be\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"8711a017-62dd-479f-b84e-123d6e3f40be\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612673,\"team_id\":2,\"email\":\"adelina.petrova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"8711a017-62dd-479f-b84e-123d6e3f40be\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612673,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"8711a017-62dd-479f-b84e-123d6e3f40be\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612673} {\"correlation_id\":\"8711a017-62dd-479f-b84e-123d6e3f40be\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612673,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"8711a017-62dd-479f-b84e-123d6e3f40be\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612673,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36} {\"correlation_id\":\"8711a017-62dd-479f-b84e-123d6e3f40be\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612642,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36}} {\"correlation_id\":\"aa7b8829-1739-4f9b-aad1-c6868d8679ca\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612642} {\"correlation_id\":\"aa7b8829-1739-4f9b-aad1-c6868d8679ca\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612642,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"aa7b8829-1739-4f9b-aad1-c6868d8679ca\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612642,\"participants\":[{\"id\":999935,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null},{\"id\":999936,\"user_id\":261,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"aa7b8829-1739-4f9b-aad1-c6868d8679ca\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"aa7b8829-1739-4f9b-aad1-c6868d8679ca\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"aa7b8829-1739-4f9b-aad1-c6868d8679ca\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"aa7b8829-1739-4f9b-aad1-c6868d8679ca\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"aa7b8829-1739-4f9b-aad1-c6868d8679ca\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"aa7b8829-1739-4f9b-aad1-c6868d8679ca\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612642,\"team_id\":2,\"email\":\"adelina.petrova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"aa7b8829-1739-4f9b-aad1-c6868d8679ca\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612642,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"aa7b8829-1739-4f9b-aad1-c6868d8679ca\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612642} {\"correlation_id\":\"aa7b8829-1739-4f9b-aad1-c6868d8679ca\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612642,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"aa7b8829-1739-4f9b-aad1-c6868d8679ca\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612642,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36} {\"correlation_id\":\"aa7b8829-1739-4f9b-aad1-c6868d8679ca\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612598,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"2fef3119-c9e0-421d-b77e-3cf1e26b480a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612598} {\"correlation_id\":\"2fef3119-c9e0-421d-b77e-3cf1e26b480a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612598,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"2fef3119-c9e0-421d-b77e-3cf1e26b480a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612598,\"participants\":[{\"id\":999857,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null},{\"id\":999858,\"user_id\":206,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"2fef3119-c9e0-421d-b77e-3cf1e26b480a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"2fef3119-c9e0-421d-b77e-3cf1e26b480a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"2fef3119-c9e0-421d-b77e-3cf1e26b480a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"2fef3119-c9e0-421d-b77e-3cf1e26b480a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"2fef3119-c9e0-421d-b77e-3cf1e26b480a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: ProspectCache - Searching DB for opportunity by owner {\"account_id\":243,\"contact_id\":4491,\"owner_id\":206} {\"correlation_id\":\"2fef3119-c9e0-421d-b77e-3cf1e26b480a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: ProspectCache - Fallback DB opportunity search {\"account_id\":243,\"contact_id\":4491} {\"correlation_id\":\"2fef3119-c9e0-421d-b77e-3cf1e26b480a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: ProspectCache - Opportunity DB search results {\"account_id\":243,\"contact_id\":4491,\"opportunity_id\":276} {\"correlation_id\":\"2fef3119-c9e0-421d-b77e-3cf1e26b480a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"2fef3119-c9e0-421d-b77e-3cf1e26b480a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612598,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"2fef3119-c9e0-421d-b77e-3cf1e26b480a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612598,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"2fef3119-c9e0-421d-b77e-3cf1e26b480a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612598} {\"correlation_id\":\"2fef3119-c9e0-421d-b77e-3cf1e26b480a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612598,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"2fef3119-c9e0-421d-b77e-3cf1e26b480a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612598,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"2fef3119-c9e0-421d-b77e-3cf1e26b480a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612597,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"f4aa8c68-1048-465c-8ab6-e414220b2b3d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612597} {\"correlation_id\":\"f4aa8c68-1048-465c-8ab6-e414220b2b3d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612597,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"f4aa8c68-1048-465c-8ab6-e414220b2b3d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612597,\"participants\":[{\"id\":999855,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999856,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null}]} {\"correlation_id\":\"f4aa8c68-1048-465c-8ab6-e414220b2b3d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"f4aa8c68-1048-465c-8ab6-e414220b2b3d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"f4aa8c68-1048-465c-8ab6-e414220b2b3d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"f4aa8c68-1048-465c-8ab6-e414220b2b3d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"f4aa8c68-1048-465c-8ab6-e414220b2b3d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612597,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"f4aa8c68-1048-465c-8ab6-e414220b2b3d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"f4aa8c68-1048-465c-8ab6-e414220b2b3d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612597,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"f4aa8c68-1048-465c-8ab6-e414220b2b3d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612597} {\"correlation_id\":\"f4aa8c68-1048-465c-8ab6-e414220b2b3d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612597,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"f4aa8c68-1048-465c-8ab6-e414220b2b3d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612597,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"f4aa8c68-1048-465c-8ab6-e414220b2b3d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612596,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"885d60db-2622-4f69-9826-b15464181dcb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612596} {\"correlation_id\":\"885d60db-2622-4f69-9826-b15464181dcb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612596,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"885d60db-2622-4f69-9826-b15464181dcb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612596,\"participants\":[{\"id\":999853,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999854,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null}]} {\"correlation_id\":\"885d60db-2622-4f69-9826-b15464181dcb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"885d60db-2622-4f69-9826-b15464181dcb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"885d60db-2622-4f69-9826-b15464181dcb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"885d60db-2622-4f69-9826-b15464181dcb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"885d60db-2622-4f69-9826-b15464181dcb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612596,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"885d60db-2622-4f69-9826-b15464181dcb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"885d60db-2622-4f69-9826-b15464181dcb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612596,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"885d60db-2622-4f69-9826-b15464181dcb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612596} {\"correlation_id\":\"885d60db-2622-4f69-9826-b15464181dcb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612596,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"885d60db-2622-4f69-9826-b15464181dcb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612596,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"885d60db-2622-4f69-9826-b15464181dcb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612595,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"0bc8a5e9-778b-4d99-bf0c-d903bd129c55\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612595} {\"correlation_id\":\"0bc8a5e9-778b-4d99-bf0c-d903bd129c55\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612595,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"0bc8a5e9-778b-4d99-bf0c-d903bd129c55\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612595,\"participants\":[{\"id\":999851,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null},{\"id\":999852,\"user_id\":206,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"0bc8a5e9-778b-4d99-bf0c-d903bd129c55\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"0bc8a5e9-778b-4d99-bf0c-d903bd129c55\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"0bc8a5e9-778b-4d99-bf0c-d903bd129c55\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"0bc8a5e9-778b-4d99-bf0c-d903bd129c55\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"0bc8a5e9-778b-4d99-bf0c-d903bd129c55\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"0bc8a5e9-778b-4d99-bf0c-d903bd129c55\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612595,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"0bc8a5e9-778b-4d99-bf0c-d903bd129c55\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612595,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"0bc8a5e9-778b-4d99-bf0c-d903bd129c55\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612595} {\"correlation_id\":\"0bc8a5e9-778b-4d99-bf0c-d903bd129c55\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612595,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"0bc8a5e9-778b-4d99-bf0c-d903bd129c55\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612595,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"0bc8a5e9-778b-4d99-bf0c-d903bd129c55\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612594,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"9710b4b5-e8f1-4fbf-b865-162ea340046c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612594} {\"correlation_id\":\"9710b4b5-e8f1-4fbf-b865-162ea340046c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612594,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"9710b4b5-e8f1-4fbf-b865-162ea340046c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612594,\"participants\":[{\"id\":999849,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999850,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null}]} {\"correlation_id\":\"9710b4b5-e8f1-4fbf-b865-162ea340046c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"9710b4b5-e8f1-4fbf-b865-162ea340046c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"9710b4b5-e8f1-4fbf-b865-162ea340046c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"9710b4b5-e8f1-4fbf-b865-162ea340046c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"9710b4b5-e8f1-4fbf-b865-162ea340046c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612594,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"9710b4b5-e8f1-4fbf-b865-162ea340046c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"9710b4b5-e8f1-4fbf-b865-162ea340046c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612594,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"9710b4b5-e8f1-4fbf-b865-162ea340046c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612594} {\"correlation_id\":\"9710b4b5-e8f1-4fbf-b865-162ea340046c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612594,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"9710b4b5-e8f1-4fbf-b865-162ea340046c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612594,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"9710b4b5-e8f1-4fbf-b865-162ea340046c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612593,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"59a8137b-0518-4c91-b63d-c794df57a8c1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612593} {\"correlation_id\":\"59a8137b-0518-4c91-b63d-c794df57a8c1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612593,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"59a8137b-0518-4c91-b63d-c794df57a8c1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612593,\"participants\":[{\"id\":999847,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999848,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null}]} {\"correlation_id\":\"59a8137b-0518-4c91-b63d-c794df57a8c1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"59a8137b-0518-4c91-b63d-c794df57a8c1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"59a8137b-0518-4c91-b63d-c794df57a8c1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"59a8137b-0518-4c91-b63d-c794df57a8c1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"59a8137b-0518-4c91-b63d-c794df57a8c1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612593,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"59a8137b-0518-4c91-b63d-c794df57a8c1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"59a8137b-0518-4c91-b63d-c794df57a8c1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612593,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"59a8137b-0518-4c91-b63d-c794df57a8c1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612593} {\"correlation_id\":\"59a8137b-0518-4c91-b63d-c794df57a8c1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612593,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"59a8137b-0518-4c91-b63d-c794df57a8c1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612593,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"59a8137b-0518-4c91-b63d-c794df57a8c1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612592,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"e1f2c9b0-a985-4277-b0da-5190fb9b25d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612592} {\"correlation_id\":\"e1f2c9b0-a985-4277-b0da-5190fb9b25d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612592,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"e1f2c9b0-a985-4277-b0da-5190fb9b25d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612592,\"participants\":[{\"id\":999845,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null},{\"id\":999846,\"user_id\":206,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"e1f2c9b0-a985-4277-b0da-5190fb9b25d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"e1f2c9b0-a985-4277-b0da-5190fb9b25d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"e1f2c9b0-a985-4277-b0da-5190fb9b25d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"e1f2c9b0-a985-4277-b0da-5190fb9b25d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"e1f2c9b0-a985-4277-b0da-5190fb9b25d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"e1f2c9b0-a985-4277-b0da-5190fb9b25d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612592,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"e1f2c9b0-a985-4277-b0da-5190fb9b25d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612592,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"e1f2c9b0-a985-4277-b0da-5190fb9b25d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612592} {\"correlation_id\":\"e1f2c9b0-a985-4277-b0da-5190fb9b25d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612592,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"e1f2c9b0-a985-4277-b0da-5190fb9b25d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612592,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"e1f2c9b0-a985-4277-b0da-5190fb9b25d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612591,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"8a5f33ea-490a-41e3-b71a-872c52626583\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612591} {\"correlation_id\":\"8a5f33ea-490a-41e3-b71a-872c52626583\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612591,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"8a5f33ea-490a-41e3-b71a-872c52626583\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612591,\"participants\":[{\"id\":999843,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999844,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null}]} {\"correlation_id\":\"8a5f33ea-490a-41e3-b71a-872c52626583\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"8a5f33ea-490a-41e3-b71a-872c52626583\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"8a5f33ea-490a-41e3-b71a-872c52626583\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"8a5f33ea-490a-41e3-b71a-872c52626583\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"8a5f33ea-490a-41e3-b71a-872c52626583\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612591,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"8a5f33ea-490a-41e3-b71a-872c52626583\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"8a5f33ea-490a-41e3-b71a-872c52626583\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612591,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"8a5f33ea-490a-41e3-b71a-872c52626583\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612591} {\"correlation_id\":\"8a5f33ea-490a-41e3-b71a-872c52626583\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612591,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"8a5f33ea-490a-41e3-b71a-872c52626583\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612591,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"8a5f33ea-490a-41e3-b71a-872c52626583\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612590,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"b34fb574-2229-4ff9-b572-7e2f801295a8\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612590} {\"correlation_id\":\"b34fb574-2229-4ff9-b572-7e2f801295a8\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612590,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"b34fb574-2229-4ff9-b572-7e2f801295a8\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612590,\"participants\":[{\"id\":999841,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999842,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null}]} {\"correlation_id\":\"b34fb574-2229-4ff9-b572-7e2f801295a8\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"b34fb574-2229-4ff9-b572-7e2f801295a8\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"b34fb574-2229-4ff9-b572-7e2f801295a8\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"b34fb574-2229-4ff9-b572-7e2f801295a8\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"b34fb574-2229-4ff9-b572-7e2f801295a8\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612590,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"b34fb574-2229-4ff9-b572-7e2f801295a8\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"b34fb574-2229-4ff9-b572-7e2f801295a8\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612590,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"b34fb574-2229-4ff9-b572-7e2f801295a8\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612590} {\"correlation_id\":\"b34fb574-2229-4ff9-b572-7e2f801295a8\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612590,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"b34fb574-2229-4ff9-b572-7e2f801295a8\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612590,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"b34fb574-2229-4ff9-b572-7e2f801295a8\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612589,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"746b5162-98d0-4e35-96ca-f6756e9b2d39\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612589} {\"correlation_id\":\"746b5162-98d0-4e35-96ca-f6756e9b2d39\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612589,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"746b5162-98d0-4e35-96ca-f6756e9b2d39\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612589,\"participants\":[{\"id\":999839,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999840,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null}]} {\"correlation_id\":\"746b5162-98d0-4e35-96ca-f6756e9b2d39\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"746b5162-98d0-4e35-96ca-f6756e9b2d39\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"746b5162-98d0-4e35-96ca-f6756e9b2d39\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"746b5162-98d0-4e35-96ca-f6756e9b2d39\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"746b5162-98d0-4e35-96ca-f6756e9b2d39\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612589,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"746b5162-98d0-4e35-96ca-f6756e9b2d39\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"746b5162-98d0-4e35-96ca-f6756e9b2d39\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612589,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"746b5162-98d0-4e35-96ca-f6756e9b2d39\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612589} {\"correlation_id\":\"746b5162-98d0-4e35-96ca-f6756e9b2d39\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612589,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"746b5162-98d0-4e35-96ca-f6756e9b2d39\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612589,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"746b5162-98d0-4e35-96ca-f6756e9b2d39\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612588,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"601702ec-6d8b-4aa3-b7e2-780e758b68d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612588} {\"correlation_id\":\"601702ec-6d8b-4aa3-b7e2-780e758b68d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612588,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"601702ec-6d8b-4aa3-b7e2-780e758b68d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612588,\"participants\":[{\"id\":999837,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null},{\"id\":999838,\"user_id\":206,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"601702ec-6d8b-4aa3-b7e2-780e758b68d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"601702ec-6d8b-4aa3-b7e2-780e758b68d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"601702ec-6d8b-4aa3-b7e2-780e758b68d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"601702ec-6d8b-4aa3-b7e2-780e758b68d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"601702ec-6d8b-4aa3-b7e2-780e758b68d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"601702ec-6d8b-4aa3-b7e2-780e758b68d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612588,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"601702ec-6d8b-4aa3-b7e2-780e758b68d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612588,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"601702ec-6d8b-4aa3-b7e2-780e758b68d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612588} {\"correlation_id\":\"601702ec-6d8b-4aa3-b7e2-780e758b68d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612588,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"601702ec-6d8b-4aa3-b7e2-780e758b68d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612588,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"601702ec-6d8b-4aa3-b7e2-780e758b68d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612587,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"75f31c21-1a08-4a7a-beef-a6ee82905eb2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612587} {\"correlation_id\":\"75f31c21-1a08-4a7a-beef-a6ee82905eb2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612587,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"75f31c21-1a08-4a7a-beef-a6ee82905eb2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612587,\"participants\":[{\"id\":999835,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999836,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null}]} {\"correlation_id\":\"75f31c21-1a08-4a7a-beef-a6ee82905eb2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"75f31c21-1a08-4a7a-beef-a6ee82905eb2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"75f31c21-1a08-4a7a-beef-a6ee82905eb2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"75f31c21-1a08-4a7a-beef-a6ee82905eb2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"75f31c21-1a08-4a7a-beef-a6ee82905eb2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612587,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"75f31c21-1a08-4a7a-beef-a6ee82905eb2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"75f31c21-1a08-4a7a-beef-a6ee82905eb2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612587,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"75f31c21-1a08-4a7a-beef-a6ee82905eb2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612587} {\"correlation_id\":\"75f31c21-1a08-4a7a-beef-a6ee82905eb2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612587,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"75f31c21-1a08-4a7a-beef-a6ee82905eb2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612587,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"75f31c21-1a08-4a7a-beef-a6ee82905eb2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612586,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"b79deb62-f26f-4831-8192-2fca40cc0445\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612586} {\"correlation_id\":\"b79deb62-f26f-4831-8192-2fca40cc0445\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612586,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"b79deb62-f26f-4831-8192-2fca40cc0445\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612586,\"participants\":[{\"id\":999833,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null},{\"id\":999834,\"user_id\":206,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"b79deb62-f26f-4831-8192-2fca40cc0445\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"b79deb62-f26f-4831-8192-2fca40cc0445\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"b79deb62-f26f-4831-8192-2fca40cc0445\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"b79deb62-f26f-4831-8192-2fca40cc0445\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"b79deb62-f26f-4831-8192-2fca40cc0445\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"b79deb62-f26f-4831-8192-2fca40cc0445\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612586,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"b79deb62-f26f-4831-8192-2fca40cc0445\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612586,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"b79deb62-f26f-4831-8192-2fca40cc0445\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612586} {\"correlation_id\":\"b79deb62-f26f-4831-8192-2fca40cc0445\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612586,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"b79deb62-f26f-4831-8192-2fca40cc0445\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612586,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"b79deb62-f26f-4831-8192-2fca40cc0445\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612585,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"2aa37843-11f3-4f11-a9ef-3277ddd89ec3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612585} {\"correlation_id\":\"2aa37843-11f3-4f11-a9ef-3277ddd89ec3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612585,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"2aa37843-11f3-4f11-a9ef-3277ddd89ec3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612585,\"participants\":[{\"id\":999831,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999832,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null}]} {\"correlation_id\":\"2aa37843-11f3-4f11-a9ef-3277ddd89ec3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"2aa37843-11f3-4f11-a9ef-3277ddd89ec3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"2aa37843-11f3-4f11-a9ef-3277ddd89ec3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"2aa37843-11f3-4f11-a9ef-3277ddd89ec3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"2aa37843-11f3-4f11-a9ef-3277ddd89ec3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612585,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"2aa37843-11f3-4f11-a9ef-3277ddd89ec3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"2aa37843-11f3-4f11-a9ef-3277ddd89ec3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612585,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"2aa37843-11f3-4f11-a9ef-3277ddd89ec3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612585} {\"correlation_id\":\"2aa37843-11f3-4f11-a9ef-3277ddd89ec3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612585,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"2aa37843-11f3-4f11-a9ef-3277ddd89ec3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612585,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"2aa37843-11f3-4f11-a9ef-3277ddd89ec3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612584,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"07bfbf03-ba7c-47ad-8252-88913ec31644\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612584} {\"correlation_id\":\"07bfbf03-ba7c-47ad-8252-88913ec31644\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612584,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"07bfbf03-ba7c-47ad-8252-88913ec31644\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612584,\"participants\":[{\"id\":999829,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999830,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null}]} {\"correlation_id\":\"07bfbf03-ba7c-47ad-8252-88913ec31644\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"07bfbf03-ba7c-47ad-8252-88913ec31644\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"07bfbf03-ba7c-47ad-8252-88913ec31644\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"07bfbf03-ba7c-47ad-8252-88913ec31644\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"07bfbf03-ba7c-47ad-8252-88913ec31644\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612584,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"07bfbf03-ba7c-47ad-8252-88913ec31644\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"07bfbf03-ba7c-47ad-8252-88913ec31644\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612584,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"07bfbf03-ba7c-47ad-8252-88913ec31644\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612584} {\"correlation_id\":\"07bfbf03-ba7c-47ad-8252-88913ec31644\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612584,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"07bfbf03-ba7c-47ad-8252-88913ec31644\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612584,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"07bfbf03-ba7c-47ad-8252-88913ec31644\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612583,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"64e9fc7f-c17e-4af3-b884-54a2c388e8f0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612583} {\"correlation_id\":\"64e9fc7f-c17e-4af3-b884-54a2c388e8f0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612583,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"64e9fc7f-c17e-4af3-b884-54a2c388e8f0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612583,\"participants\":[{\"id\":999827,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null},{\"id\":999828,\"user_id\":206,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"64e9fc7f-c17e-4af3-b884-54a2c388e8f0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"64e9fc7f-c17e-4af3-b884-54a2c388e8f0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"64e9fc7f-c17e-4af3-b884-54a2c388e8f0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"64e9fc7f-c17e-4af3-b884-54a2c388e8f0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"64e9fc7f-c17e-4af3-b884-54a2c388e8f0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"64e9fc7f-c17e-4af3-b884-54a2c388e8f0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612583,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"64e9fc7f-c17e-4af3-b884-54a2c388e8f0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612583,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"64e9fc7f-c17e-4af3-b884-54a2c388e8f0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612583} {\"correlation_id\":\"64e9fc7f-c17e-4af3-b884-54a2c388e8f0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612583,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"64e9fc7f-c17e-4af3-b884-54a2c388e8f0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612583,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"64e9fc7f-c17e-4af3-b884-54a2c388e8f0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612582,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"23188089-fcae-491d-b35d-c20491564cfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612582} {\"correlation_id\":\"23188089-fcae-491d-b35d-c20491564cfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612582,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"23188089-fcae-491d-b35d-c20491564cfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612582,\"participants\":[{\"id\":999825,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999826,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null}]} {\"correlation_id\":\"23188089-fcae-491d-b35d-c20491564cfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"23188089-fcae-491d-b35d-c20491564cfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"23188089-fcae-491d-b35d-c20491564cfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"23188089-fcae-491d-b35d-c20491564cfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"23188089-fcae-491d-b35d-c20491564cfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612582,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"23188089-fcae-491d-b35d-c20491564cfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"23188089-fcae-491d-b35d-c20491564cfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612582,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"23188089-fcae-491d-b35d-c20491564cfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612582} {\"correlation_id\":\"23188089-fcae-491d-b35d-c20491564cfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612582,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"23188089-fcae-491d-b35d-c20491564cfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612582,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"23188089-fcae-491d-b35d-c20491564cfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"9f9cc209-aee5-4d58-b4ad-1455693e83d6\",\"trace_id\":\"f11e2691-d834-4325-91f7-003e08888b34\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612581,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"91305be5-0942-40ad-86c8-c2b2f018ea2b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612581} {\"correlation_id\":\"91305be5-0942-40ad-86c8-c2b2f018ea2b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612581,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"91305be5-0942-40ad-86c8-c2b2f018ea2b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612581,\"participants\":[{\"id\":999823,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null},{\"id\":999824,\"user_id\":206,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"91305be5-0942-40ad-86c8-c2b2f018ea2b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"91305be5-0942-40ad-86c8-c2b2f018ea2b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"91305be5-0942-40ad-86c8-c2b2f018ea2b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"91305be5-0942-40ad-86c8-c2b2f018ea2b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"91305be5-0942-40ad-86c8-c2b2f018ea2b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"91305be5-0942-40ad-86c8-c2b2f018ea2b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612581,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"91305be5-0942-40ad-86c8-c2b2f018ea2b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612581,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"91305be5-0942-40ad-86c8-c2b2f018ea2b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612581} {\"correlation_id\":\"91305be5-0942-40ad-86c8-c2b2f018ea2b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612581,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"91305be5-0942-40ad-86c8-c2b2f018ea2b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [EmailSchedule] STARTING batch process {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"9f9cc209-aee5-4d58-b4ad-1455693e83d6\",\"trace_id\":\"f11e2691-d834-4325-91f7-003e08888b34\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612581,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"91305be5-0942-40ad-86c8-c2b2f018ea2b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612565,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"be6ed1a4-4495-4ec9-932f-da6698c31cec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [EmailSchedule] FINISHED batch process {\"host\":\"docker_lamp_1\",\"processed\":0} {\"correlation_id\":\"9f9cc209-aee5-4d58-b4ad-1455693e83d6\",\"trace_id\":\"f11e2691-d834-4325-91f7-003e08888b34\"}\n[2026-05-11 10:43:29] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":60.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"9f9cc209-aee5-4d58-b4ad-1455693e83d6\",\"trace_id\":\"f11e2691-d834-4325-91f7-003e08888b34\"}\n[2026-05-11 10:43:29] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612565} {\"correlation_id\":\"be6ed1a4-4495-4ec9-932f-da6698c31cec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612565,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"be6ed1a4-4495-4ec9-932f-da6698c31cec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612565,\"participants\":[{\"id\":999789,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999790,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null}]} {\"correlation_id\":\"be6ed1a4-4495-4ec9-932f-da6698c31cec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"be6ed1a4-4495-4ec9-932f-da6698c31cec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"be6ed1a4-4495-4ec9-932f-da6698c31cec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"be6ed1a4-4495-4ec9-932f-da6698c31cec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"be6ed1a4-4495-4ec9-932f-da6698c31cec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612565,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"be6ed1a4-4495-4ec9-932f-da6698c31cec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"be6ed1a4-4495-4ec9-932f-da6698c31cec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612565,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"be6ed1a4-4495-4ec9-932f-da6698c31cec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612565} {\"correlation_id\":\"be6ed1a4-4495-4ec9-932f-da6698c31cec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612565,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"be6ed1a4-4495-4ec9-932f-da6698c31cec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612565,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"be6ed1a4-4495-4ec9-932f-da6698c31cec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612563,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":350,\"stage_id\":34}} {\"correlation_id\":\"77b39c9c-1e21-4c1d-925b-4acce1b204d3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612563} {\"correlation_id\":\"77b39c9c-1e21-4c1d-925b-4acce1b204d3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612563,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"77b39c9c-1e21-4c1d-925b-4acce1b204d3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612563,\"participants\":[{\"id\":999784,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999785,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null}]} {\"correlation_id\":\"77b39c9c-1e21-4c1d-925b-4acce1b204d3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"77b39c9c-1e21-4c1d-925b-4acce1b204d3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"77b39c9c-1e21-4c1d-925b-4acce1b204d3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"77b39c9c-1e21-4c1d-925b-4acce1b204d3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"77b39c9c-1e21-4c1d-925b-4acce1b204d3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612563,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"77b39c9c-1e21-4c1d-925b-4acce1b204d3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: ProspectCache - Searching DB for opportunity by owner {\"account_id\":244,\"contact_id\":4487,\"owner_id\":206} {\"correlation_id\":\"77b39c9c-1e21-4c1d-925b-4acce1b204d3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: ProspectCache - Fallback DB opportunity search {\"account_id\":244,\"contact_id\":4487} {\"correlation_id\":\"77b39c9c-1e21-4c1d-925b-4acce1b204d3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: ProspectCache - Opportunity DB search results {\"account_id\":244,\"contact_id\":4487,\"opportunity_id\":350} {\"correlation_id\":\"77b39c9c-1e21-4c1d-925b-4acce1b204d3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"77b39c9c-1e21-4c1d-925b-4acce1b204d3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:30] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612563,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"77b39c9c-1e21-4c1d-925b-4acce1b204d3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:30] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612563} {\"correlation_id\":\"77b39c9c-1e21-4c1d-925b-4acce1b204d3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:30] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612563,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"77b39c9c-1e21-4c1d-925b-4acce1b204d3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:30] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612563,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":350,\"stage_id\":34} {\"correlation_id\":\"77b39c9c-1e21-4c1d-925b-4acce1b204d3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:30] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612562,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"485e0be1-a971-4cde-9ead-4276768bdf66\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:30] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612562} {\"correlation_id\":\"485e0be1-a971-4cde-9ead-4276768bdf66\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:30] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612562,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"485e0be1-a971-4cde-9ead-4276768bdf66\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:30] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612562,\"participants\":[{\"id\":999782,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":999783,\"user_id\":206,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"485e0be1-a971-4cde-9ead-4276768bdf66\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:30] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"485e0be1-a971-4cde-9ead-4276768bdf66\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:30] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"485e0be1-a971-4cde-9ead-4276768bdf66\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:30] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"485e0be1-a971-4cde-9ead-4276768bdf66\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:30] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"485e0be1-a971-4cde-9ead-4276768bdf66\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:30] local.INFO: [Prospect match] Cache miss, calling the API {\"identifier_type\":\"email\",\"identifier\":\"447782589921@txt.staging.jiminny.com\"} {\"correlation_id\":\"485e0be1-a971-4cde-9ead-4276768bdf66\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:30] local.INFO: [Hubspot] Failed to fetch contact {\"email\":\"447782589921@txt.staging.jiminny.com\",\"reason\":\"[404] Client error: `GET https://api.hubapi.com/crm/v3/objects/contacts/447782589921%40txt.staging.jiminny.com?properties=email%2Cfirstname%2Clastname%2Ccountry%2Cphone%2Cmobilephone%2Cjobtitle%2Chubspot_owner_id%2Cassociatedcompanyid%2Cphoto&archived=0&idProperty=email` resulted in a `404 Not Found` response\"} {\"correlation_id\":\"485e0be1-a971-4cde-9ead-4276768bdf66\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:30] local.INFO: [Prospect match] API returned empty result, caching the miss with empty prospect data {\"identifier_type\":\"email\",\"identifier\":\"447782589921@txt.staging.jiminny.com\"} {\"correlation_id\":\"485e0be1-a971-4cde-9ead-4276768bdf66\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:30] local.INFO: [Hubspot] Pagination completed {\"team_id\":2,\"endpoint\":\"https://api.hubapi.com/crm/v3/objects/contact/search\",\"total_requests\":1,\"total_records_fetched\":0,\"total_elapsed_seconds\":0.22,\"average_seconds_per_request\":0.22} {\"correlation_id\":\"485e0be1-a971-4cde-9ead-4276768bdf66\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:30] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"447782589921@txt.staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"485e0be1-a971-4cde-9ead-4276768bdf66\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:30] local.INFO: [Prospect match] Cache miss {\"identifier_type\":\"domain\",\"identifier\":\"jiminny.com\",\"crm\":\"hubspot\"} {\"correlation_id\":\"485e0be1-a971-4cde-9ead-4276768bdf66\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:30] local.INFO: [Prospect match] Cache miss, calling the API {\"identifier_type\":\"domain\",\"identifier\":\"jiminny.com\"} {\"correlation_id\":\"485e0be1-a971-4cde-9ead-4276768bdf66\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:31] local.INFO: [HubSpot] importAccount {\"crm_provider_id\":\"749766179\",\"config_id\":2} {\"correlation_id\":\"485e0be1-a971-4cde-9ead-4276768bdf66\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:31] local.INFO: [HubSpot] CRM Search requested {\"request\":{\"filterGroups\":[{\"filters\":[{\"propertyName\":\"associations.company\",\"operator\":\"EQ\",\"value\":\"749766179\"},{\"propertyName\":\"dealstage\",\"operator\":\"NOT_IN\",\"values\":[\"closedwon\",\"4040964\",\"59247967\"]},{\"propertyName\":\"dealstage\",\"operator\":\"NOT_IN\",\"values\":[\"closedlost\",\"4040965\",\"59247968\"]}]}],\"sorts\":[{\"propertyName\":\"modifieddate\",\"direction\":\"DESCENDING\"}],\"properties\":[\"dealname\",\"amount\",\"hubspot_owner_id\",\"pipeline\",\"dealstage\",\"closedate\",\"deal_currency_code\"],\"limit\":200}} {\"correlation_id\":\"485e0be1-a971-4cde-9ead-4276768bdf66\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:31] local.INFO: [Hubspot] Pagination completed {\"team_id\":2,\"endpoint\":\"https://api.hubapi.com/crm/v3/objects/deals/search\",\"total_requests\":1,\"total_records_fetched\":10,\"total_elapsed_seconds\":0.3,\"average_seconds_per_request\":0.3} {\"correlation_id\":\"485e0be1-a971-4cde-9ead-4276768bdf66\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612562,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"485e0be1-a971-4cde-9ead-4276768bdf66\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612562,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"485e0be1-a971-4cde-9ead-4276768bdf66\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612562} {\"correlation_id\":\"485e0be1-a971-4cde-9ead-4276768bdf66\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612562,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"485e0be1-a971-4cde-9ead-4276768bdf66\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612562,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"485e0be1-a971-4cde-9ead-4276768bdf66\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612561,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"9fffce69-30ed-4c66-9ee9-e980e776cd57\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612561} {\"correlation_id\":\"9fffce69-30ed-4c66-9ee9-e980e776cd57\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612561,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"9fffce69-30ed-4c66-9ee9-e980e776cd57\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612561,\"participants\":[{\"id\":999780,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999781,\"user_id\":null,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"9fffce69-30ed-4c66-9ee9-e980e776cd57\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"9fffce69-30ed-4c66-9ee9-e980e776cd57\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"9fffce69-30ed-4c66-9ee9-e980e776cd57\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"9fffce69-30ed-4c66-9ee9-e980e776cd57\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"9fffce69-30ed-4c66-9ee9-e980e776cd57\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612561,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"9fffce69-30ed-4c66-9ee9-e980e776cd57\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [Prospect match] Cache miss, calling the API {\"identifier_type\":\"email\",\"identifier\":\"447700174614.447782589921.OeREojLVnk@txt.staging.jiminny.com\"} {\"correlation_id\":\"9fffce69-30ed-4c66-9ee9-e980e776cd57\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [Hubspot] Failed to fetch contact {\"email\":\"447700174614.447782589921.OeREojLVnk@txt.staging.jiminny.com\",\"reason\":\"[404] Client error: `GET https://api.hubapi.com/crm/v3/objects/contacts/447700174614.447782589921.OeREojLVnk%40txt.staging.jiminny.com?properties=email%2Cfirstname%2Clastname%2Ccountry%2Cphone%2Cmobilephone%2Cjobtitle%2Chubspot_owner_id%2Cassociatedcompanyid%2Cphoto&archived=0&idProperty=email` resulted in a `404 Not Found` response\"} {\"correlation_id\":\"9fffce69-30ed-4c66-9ee9-e980e776cd57\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [Prospect match] API returned empty result, caching the miss with empty prospect data {\"identifier_type\":\"email\",\"identifier\":\"447700174614.447782589921.OeREojLVnk@txt.staging.jiminny.com\"} {\"correlation_id\":\"9fffce69-30ed-4c66-9ee9-e980e776cd57\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"447700174614.447782589921.OeREojLVnk@txt.staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"9fffce69-30ed-4c66-9ee9-e980e776cd57\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612561,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"9fffce69-30ed-4c66-9ee9-e980e776cd57\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612561} {\"correlation_id\":\"9fffce69-30ed-4c66-9ee9-e980e776cd57\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612561,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"9fffce69-30ed-4c66-9ee9-e980e776cd57\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612561,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"9fffce69-30ed-4c66-9ee9-e980e776cd57\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612560,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"6a946453-4ffe-4df1-b91d-f73baf850a7a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612560} {\"correlation_id\":\"6a946453-4ffe-4df1-b91d-f73baf850a7a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612560,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"6a946453-4ffe-4df1-b91d-f73baf850a7a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612560,\"participants\":[{\"id\":999778,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":999779,\"user_id\":206,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"6a946453-4ffe-4df1-b91d-f73baf850a7a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"6a946453-4ffe-4df1-b91d-f73baf850a7a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"6a946453-4ffe-4df1-b91d-f73baf850a7a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"6a946453-4ffe-4df1-b91d-f73baf850a7a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"6a946453-4ffe-4df1-b91d-f73baf850a7a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"447782589921@txt.staging.jiminny.com\"} {\"correlation_id\":\"6a946453-4ffe-4df1-b91d-f73baf850a7a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"447782589921@txt.staging.jiminny.com\"} {\"correlation_id\":\"6a946453-4ffe-4df1-b91d-f73baf850a7a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"447782589921@txt.staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"6a946453-4ffe-4df1-b91d-f73baf850a7a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612560,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"6a946453-4ffe-4df1-b91d-f73baf850a7a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612560,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"6a946453-4ffe-4df1-b91d-f73baf850a7a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612560} {\"correlation_id\":\"6a946453-4ffe-4df1-b91d-f73baf850a7a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612560,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"6a946453-4ffe-4df1-b91d-f73baf850a7a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612560,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"6a946453-4ffe-4df1-b91d-f73baf850a7a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612559,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":5011,\"stage_id\":34}} {\"correlation_id\":\"6231af67-0852-4f81-a268-953b6db0b3aa\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612559} {\"correlation_id\":\"6231af67-0852-4f81-a268-953b6db0b3aa\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612559,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"6231af67-0852-4f81-a268-953b6db0b3aa\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612559,\"participants\":[{\"id\":999776,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999777,\"user_id\":null,\"contact_id\":97,\"lead_id\":null}]} {\"correlation_id\":\"6231af67-0852-4f81-a268-953b6db0b3aa\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"6231af67-0852-4f81-a268-953b6db0b3aa\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"6231af67-0852-4f81-a268-953b6db0b3aa\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"6231af67-0852-4f81-a268-953b6db0b3aa\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"6231af67-0852-4f81-a268-953b6db0b3aa\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612559,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"6231af67-0852-4f81-a268-953b6db0b3aa\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: ProspectCache - Searching DB for opportunity by owner {\"account_id\":69,\"contact_id\":97,\"owner_id\":206} {\"correlation_id\":\"6231af67-0852-4f81-a268-953b6db0b3aa\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: ProspectCache - Fallback DB opportunity search {\"account_id\":69,\"contact_id\":97} {\"correlation_id\":\"6231af67-0852-4f81-a268-953b6db0b3aa\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: ProspectCache - Opportunity DB search results {\"account_id\":69,\"contact_id\":97,\"opportunity_id\":5011} {\"correlation_id\":\"6231af67-0852-4f81-a268-953b6db0b3aa\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinsoncrusoe@test.com\"} {\"correlation_id\":\"6231af67-0852-4f81-a268-953b6db0b3aa\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612559,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"6231af67-0852-4f81-a268-953b6db0b3aa\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612559} {\"correlation_id\":\"6231af67-0852-4f81-a268-953b6db0b3aa\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612559,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"6231af67-0852-4f81-a268-953b6db0b3aa\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612559,\"remote_search\":true,\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":5011,\"stage_id\":34} {\"correlation_id\":\"6231af67-0852-4f81-a268-953b6db0b3aa\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612558,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":5011,\"stage_id\":34}} {\"correlation_id\":\"f8c4783d-18f2-45b6-a1b8-e9d80b788104\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612558} {\"correlation_id\":\"f8c4783d-18f2-45b6-a1b8-e9d80b788104\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612558,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"f8c4783d-18f2-45b6-a1b8-e9d80b788104\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612558,\"participants\":[{\"id\":999774,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999775,\"user_id\":null,\"contact_id\":97,\"lead_id\":null}]} {\"correlation_id\":\"f8c4783d-18f2-45b6-a1b8-e9d80b788104\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"f8c4783d-18f2-45b6-a1b8-e9d80b788104\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"f8c4783d-18f2-45b6-a1b8-e9d80b788104\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"f8c4783d-18f2-45b6-a1b8-e9d80b788104\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"f8c4783d-18f2-45b6-a1b8-e9d80b788104\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612558,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"f8c4783d-18f2-45b6-a1b8-e9d80b788104\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinsoncrusoe@test.com\"} {\"correlation_id\":\"f8c4783d-18f2-45b6-a1b8-e9d80b788104\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612558,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"f8c4783d-18f2-45b6-a1b8-e9d80b788104\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612558} {\"correlation_id\":\"f8c4783d-18f2-45b6-a1b8-e9d80b788104\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612558,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"f8c4783d-18f2-45b6-a1b8-e9d80b788104\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612558,\"remote_search\":true,\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":5011,\"stage_id\":34} {\"correlation_id\":\"f8c4783d-18f2-45b6-a1b8-e9d80b788104\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612557,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":5011,\"stage_id\":34}} {\"correlation_id\":\"d087a635-67a6-4fb4-be19-85f309031ebb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612557} {\"correlation_id\":\"d087a635-67a6-4fb4-be19-85f309031ebb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612557,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"d087a635-67a6-4fb4-be19-85f309031ebb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612557,\"participants\":[{\"id\":999772,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999773,\"user_id\":null,\"contact_id\":97,\"lead_id\":null}]} {\"correlation_id\":\"d087a635-67a6-4fb4-be19-85f309031ebb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"d087a635-67a6-4fb4-be19-85f309031ebb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"d087a635-67a6-4fb4-be19-85f309031ebb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d087a635-67a6-4fb4-be19-85f309031ebb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"d087a635-67a6-4fb4-be19-85f309031ebb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612557,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"d087a635-67a6-4fb4-be19-85f309031ebb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinsoncrusoe@test.com\"} {\"correlation_id\":\"d087a635-67a6-4fb4-be19-85f309031ebb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612557,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"d087a635-67a6-4fb4-be19-85f309031ebb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612557} {\"correlation_id\":\"d087a635-67a6-4fb4-be19-85f309031ebb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612557,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"d087a635-67a6-4fb4-be19-85f309031ebb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612557,\"remote_search\":true,\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":5011,\"stage_id\":34} {\"correlation_id\":\"d087a635-67a6-4fb4-be19-85f309031ebb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612556,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":5011,\"stage_id\":34}} {\"correlation_id\":\"462bb4b0-f644-4f69-b4a8-6030370c2202\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612556} {\"correlation_id\":\"462bb4b0-f644-4f69-b4a8-6030370c2202\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612556,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"462bb4b0-f644-4f69-b4a8-6030370c2202\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612556,\"participants\":[{\"id\":999770,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999771,\"user_id\":null,\"contact_id\":97,\"lead_id\":null}]} {\"correlation_id\":\"462bb4b0-f644-4f69-b4a8-6030370c2202\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"462bb4b0-f644-4f69-b4a8-6030370c2202\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"462bb4b0-f644-4f69-b4a8-6030370c2202\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"462bb4b0-f644-4f69-b4a8-6030370c2202\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"462bb4b0-f644-4f69-b4a8-6030370c2202\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612556,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"462bb4b0-f644-4f69-b4a8-6030370c2202\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinsoncrusoe@test.com\"} {\"correlation_id\":\"462bb4b0-f644-4f69-b4a8-6030370c2202\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612556,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"462bb4b0-f644-4f69-b4a8-6030370c2202\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612556} {\"correlation_id\":\"462bb4b0-f644-4f69-b4a8-6030370c2202\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612556,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"462bb4b0-f644-4f69-b4a8-6030370c2202\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612556,\"remote_search\":true,\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":5011,\"stage_id\":34} {\"correlation_id\":\"462bb4b0-f644-4f69-b4a8-6030370c2202\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612555,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":5011,\"stage_id\":34}} {\"correlation_id\":\"988eea33-83d6-41f7-93a3-9737c43ee6a3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612555} {\"correlation_id\":\"988eea33-83d6-41f7-93a3-9737c43ee6a3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [ EsUpdateProcessManager ] Finished updating entities in ES {\"worker\":\"\",\"peak_memory\":\"99.73 MB\",\"elapsed_seconds\":4.12,\"update_target\":\"activities\",\"should_iterate_again\":false} {\"correlation_id\":\"4ad05333-9afb-492e-9f0f-b2909ac45b32\",\"trace_id\":\"3d8feb24-b173-4158-b0a4-4cf33af85066\"}\n[2026-05-11 10:43:33] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612555,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"988eea33-83d6-41f7-93a3-9737c43ee6a3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612555,\"participants\":[{\"id\":999768,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999769,\"user_id\":null,\"contact_id\":97,\"lead_id\":null}]} {\"correlation_id\":\"988eea33-83d6-41f7-93a3-9737c43ee6a3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"988eea33-83d6-41f7-93a3-9737c43ee6a3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"988eea33-83d6-41f7-93a3-9737c43ee6a3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"988eea33-83d6-41f7-93a3-9737c43ee6a3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"988eea33-83d6-41f7-93a3-9737c43ee6a3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612555,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"988eea33-83d6-41f7-93a3-9737c43ee6a3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinsoncrusoe@test.com\"} {\"correlation_id\":\"988eea33-83d6-41f7-93a3-9737c43ee6a3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612555,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"988eea33-83d6-41f7-93a3-9737c43ee6a3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612555} {\"correlation_id\":\"988eea33-83d6-41f7-93a3-9737c43ee6a3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612555,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"988eea33-83d6-41f7-93a3-9737c43ee6a3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612555,\"remote_search\":true,\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":5011,\"stage_id\":34} {\"correlation_id\":\"988eea33-83d6-41f7-93a3-9737c43ee6a3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612554,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":5011,\"stage_id\":34}} {\"correlation_id\":\"7ff43be6-a32a-412c-bb3d-1bd40aa30f65\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612554} {\"correlation_id\":\"7ff43be6-a32a-412c-bb3d-1bd40aa30f65\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612554,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"7ff43be6-a32a-412c-bb3d-1bd40aa30f65\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612554,\"participants\":[{\"id\":999766,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999767,\"user_id\":null,\"contact_id\":97,\"lead_id\":null}]} {\"correlation_id\":\"7ff43be6-a32a-412c-bb3d-1bd40aa30f65\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"7ff43be6-a32a-412c-bb3d-1bd40aa30f65\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"7ff43be6-a32a-412c-bb3d-1bd40aa30f65\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"7ff43be6-a32a-412c-bb3d-1bd40aa30f65\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"7ff43be6-a32a-412c-bb3d-1bd40aa30f65\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612554,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"7ff43be6-a32a-412c-bb3d-1bd40aa30f65\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinsoncrusoe@test.com\"} {\"correlation_id\":\"7ff43be6-a32a-412c-bb3d-1bd40aa30f65\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612554,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"7ff43be6-a32a-412c-bb3d-1bd40aa30f65\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612554} {\"correlation_id\":\"7ff43be6-a32a-412c-bb3d-1bd40aa30f65\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612554,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"7ff43be6-a32a-412c-bb3d-1bd40aa30f65\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612554,\"remote_search\":true,\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":5011,\"stage_id\":34} {\"correlation_id\":\"7ff43be6-a32a-412c-bb3d-1bd40aa30f65\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612553,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":5011,\"stage_id\":34}} {\"correlation_id\":\"3e6b8bf5-c432-4502-a026-aaf235b3b359\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612553} {\"correlation_id\":\"3e6b8bf5-c432-4502-a026-aaf235b3b359\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612553,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"3e6b8bf5-c432-4502-a026-aaf235b3b359\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612553,\"participants\":[{\"id\":999764,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999765,\"user_id\":null,\"contact_id\":97,\"lead_id\":null}]} {\"correlation_id\":\"3e6b8bf5-c432-4502-a026-aaf235b3b359\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"3e6b8bf5-c432-4502-a026-aaf235b3b359\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"3e6b8bf5-c432-4502-a026-aaf235b3b359\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"3e6b8bf5-c432-4502-a026-aaf235b3b359\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"3e6b8bf5-c432-4502-a026-aaf235b3b359\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612553,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"3e6b8bf5-c432-4502-a026-aaf235b3b359\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinsoncrusoe@test.com\"} {\"correlation_id\":\"3e6b8bf5-c432-4502-a026-aaf235b3b359\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612553,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"3e6b8bf5-c432-4502-a026-aaf235b3b359\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612553} {\"correlation_id\":\"3e6b8bf5-c432-4502-a026-aaf235b3b359\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612553,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"3e6b8bf5-c432-4502-a026-aaf235b3b359\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612553,\"remote_search\":true,\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":5011,\"stage_id\":34} {\"correlation_id\":\"3e6b8bf5-c432-4502-a026-aaf235b3b359\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"calendar:sync\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:34] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612552,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":5011,\"stage_id\":34}} {\"correlation_id\":\"1d70d062-f785-4c2c-81be-d992f1ef25c0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612552} {\"correlation_id\":\"1d70d062-f785-4c2c-81be-d992f1ef25c0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:retry-failed\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"c2dd1ea7-a2bd-44ee-95a4-60f88c2fcef7\",\"trace_id\":\"dcda0937-682f-46f0-bdc7-d2054de63c89\"}\n[2026-05-11 10:43:34] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612552,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"1d70d062-f785-4c2c-81be-d992f1ef25c0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612552,\"participants\":[{\"id\":999762,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999763,\"user_id\":null,\"contact_id\":97,\"lead_id\":null}]} {\"correlation_id\":\"1d70d062-f785-4c2c-81be-d992f1ef25c0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.NOTICE: Calendar sync start {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:34] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"1d70d062-f785-4c2c-81be-d992f1ef25c0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"1d70d062-f785-4c2c-81be-d992f1ef25c0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"1d70d062-f785-4c2c-81be-d992f1ef25c0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"1d70d062-f785-4c2c-81be-d992f1ef25c0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612552,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"1d70d062-f785-4c2c-81be-d992f1ef25c0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinsoncrusoe@test.com\"} {\"correlation_id\":\"1d70d062-f785-4c2c-81be-d992f1ef25c0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612552,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"1d70d062-f785-4c2c-81be-d992f1ef25c0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612552} {\"correlation_id\":\"1d70d062-f785-4c2c-81be-d992f1ef25c0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612552,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"1d70d062-f785-4c2c-81be-d992f1ef25c0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612552,\"remote_search\":true,\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":5011,\"stage_id\":34} {\"correlation_id\":\"1d70d062-f785-4c2c-81be-d992f1ef25c0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:retry-failed\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":60.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"c2dd1ea7-a2bd-44ee-95a4-60f88c2fcef7\",\"trace_id\":\"dcda0937-682f-46f0-bdc7-d2054de63c89\"}\n[2026-05-11 10:43:34] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612551,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":5011,\"stage_id\":34}} {\"correlation_id\":\"35f5694e-2521-4089-a5f1-4adc747a40bf\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612551} {\"correlation_id\":\"35f5694e-2521-4089-a5f1-4adc747a40bf\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612551,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"35f5694e-2521-4089-a5f1-4adc747a40bf\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612551,\"participants\":[{\"id\":999760,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999761,\"user_id\":null,\"contact_id\":97,\"lead_id\":null}]} {\"correlation_id\":\"35f5694e-2521-4089-a5f1-4adc747a40bf\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"35f5694e-2521-4089-a5f1-4adc747a40bf\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"35f5694e-2521-4089-a5f1-4adc747a40bf\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"35f5694e-2521-4089-a5f1-4adc747a40bf\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"35f5694e-2521-4089-a5f1-4adc747a40bf\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612551,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"35f5694e-2521-4089-a5f1-4adc747a40bf\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinsoncrusoe@test.com\"} {\"correlation_id\":\"35f5694e-2521-4089-a5f1-4adc747a40bf\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612551,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"35f5694e-2521-4089-a5f1-4adc747a40bf\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612551} {\"correlation_id\":\"35f5694e-2521-4089-a5f1-4adc747a40bf\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612551,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"35f5694e-2521-4089-a5f1-4adc747a40bf\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612551,\"remote_search\":true,\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":5011,\"stage_id\":34} {\"correlation_id\":\"35f5694e-2521-4089-a5f1-4adc747a40bf\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1393,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:34] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1393,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:34] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612550,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":5011,\"stage_id\":34}} {\"correlation_id\":\"046f384b-a938-4138-a9e6-7df0ff6eb90b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:34] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612550} {\"correlation_id\":\"046f384b-a938-4138-a9e6-7df0ff6eb90b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612550,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"046f384b-a938-4138-a9e6-7df0ff6eb90b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1393,\"provider\":\"google\",\"refreshToken\":\"5aa7e2d96b53201cd16fca5d2e4ef3ad03320971fc064781d18aee3ae7b99fbf\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:34] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612550,\"participants\":[{\"id\":999758,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999759,\"user_id\":null,\"contact_id\":97,\"lead_id\":null}]} {\"correlation_id\":\"046f384b-a938-4138-a9e6-7df0ff6eb90b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"046f384b-a938-4138-a9e6-7df0ff6eb90b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"046f384b-a938-4138-a9e6-7df0ff6eb90b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"046f384b-a938-4138-a9e6-7df0ff6eb90b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"046f384b-a938-4138-a9e6-7df0ff6eb90b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612550,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"046f384b-a938-4138-a9e6-7df0ff6eb90b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinsoncrusoe@test.com\"} {\"correlation_id\":\"046f384b-a938-4138-a9e6-7df0ff6eb90b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612550,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"046f384b-a938-4138-a9e6-7df0ff6eb90b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612550} {\"correlation_id\":\"046f384b-a938-4138-a9e6-7df0ff6eb90b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612550,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"046f384b-a938-4138-a9e6-7df0ff6eb90b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612550,\"remote_search\":true,\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":5011,\"stage_id\":34} {\"correlation_id\":\"046f384b-a938-4138-a9e6-7df0ff6eb90b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612549,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":5011,\"stage_id\":34}} {\"correlation_id\":\"1fe4380c-c6ba-40c4-9bdc-65cf0a157cda\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612549} {\"correlation_id\":\"1fe4380c-c6ba-40c4-9bdc-65cf0a157cda\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612549,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"1fe4380c-c6ba-40c4-9bdc-65cf0a157cda\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612549,\"participants\":[{\"id\":999756,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999757,\"user_id\":null,\"contact_id\":97,\"lead_id\":null}]} {\"correlation_id\":\"1fe4380c-c6ba-40c4-9bdc-65cf0a157cda\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1393,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Account has been deleted\"}} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:34] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:34] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1393,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:34] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"1fe4380c-c6ba-40c4-9bdc-65cf0a157cda\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"1fe4380c-c6ba-40c4-9bdc-65cf0a157cda\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"1fe4380c-c6ba-40c4-9bdc-65cf0a157cda\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"1fe4380c-c6ba-40c4-9bdc-65cf0a157cda\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1387,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:34] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1387,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:34] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:34] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612549,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"1fe4380c-c6ba-40c4-9bdc-65cf0a157cda\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1387,\"provider\":\"google\",\"refreshToken\":\"8157ac6de94842937194009e9c50e459253600f799dacf6a40755ffdbeb5bba6\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:34] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinsoncrusoe@test.com\"} {\"correlation_id\":\"1fe4380c-c6ba-40c4-9bdc-65cf0a157cda\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612549,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"1fe4380c-c6ba-40c4-9bdc-65cf0a157cda\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612549} {\"correlation_id\":\"1fe4380c-c6ba-40c4-9bdc-65cf0a157cda\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612549,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"1fe4380c-c6ba-40c4-9bdc-65cf0a157cda\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612549,\"remote_search\":true,\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":5011,\"stage_id\":34} {\"correlation_id\":\"1fe4380c-c6ba-40c4-9bdc-65cf0a157cda\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612365,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"9f080e5f-9e25-468c-9d04-5cdf618dc528\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612365} {\"correlation_id\":\"9f080e5f-9e25-468c-9d04-5cdf618dc528\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612365,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"9f080e5f-9e25-468c-9d04-5cdf618dc528\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612365,\"participants\":[{\"id\":999563,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null},{\"id\":999564,\"user_id\":206,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"9f080e5f-9e25-468c-9d04-5cdf618dc528\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"9f080e5f-9e25-468c-9d04-5cdf618dc528\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"9f080e5f-9e25-468c-9d04-5cdf618dc528\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"9f080e5f-9e25-468c-9d04-5cdf618dc528\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1387,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Account has been deleted\"}} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"9f080e5f-9e25-468c-9d04-5cdf618dc528\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1387,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"9f080e5f-9e25-468c-9d04-5cdf618dc528\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612365,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"9f080e5f-9e25-468c-9d04-5cdf618dc528\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1348,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612365,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"9f080e5f-9e25-468c-9d04-5cdf618dc528\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1348,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612365} {\"correlation_id\":\"9f080e5f-9e25-468c-9d04-5cdf618dc528\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612365,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"9f080e5f-9e25-468c-9d04-5cdf618dc528\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1348,\"provider\":\"google\",\"refreshToken\":\"9e7d13d3032d0cb1b79d8e95aef01383e8e91eb52ff8ee960c8a0b6b95cd8c73\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612365,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"9f080e5f-9e25-468c-9d04-5cdf618dc528\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612360,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"e1af9830-ccba-44db-a1a0-d9ac3ec0da4d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612360} {\"correlation_id\":\"e1af9830-ccba-44db-a1a0-d9ac3ec0da4d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612360,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"e1af9830-ccba-44db-a1a0-d9ac3ec0da4d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612360,\"participants\":[{\"id\":999552,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null},{\"id\":999553,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999565,\"user_id\":null,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"e1af9830-ccba-44db-a1a0-d9ac3ec0da4d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"e1af9830-ccba-44db-a1a0-d9ac3ec0da4d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"e1af9830-ccba-44db-a1a0-d9ac3ec0da4d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"e1af9830-ccba-44db-a1a0-d9ac3ec0da4d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"e1af9830-ccba-44db-a1a0-d9ac3ec0da4d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"e1af9830-ccba-44db-a1a0-d9ac3ec0da4d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612360,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"e1af9830-ccba-44db-a1a0-d9ac3ec0da4d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1348,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Bad Request\"}} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1348,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1361,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1361,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1361,\"provider\":\"google\",\"refreshToken\":\"6c843da199c2b9907445329304fcc4ec5057a4ee748d8299641764395c08e1fd\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1361,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Account has been deleted\"}} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1361,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [Hubspot] Pagination completed {\"team_id\":2,\"endpoint\":\"https://api.hubapi.com/crm/v3/objects/contact/search\",\"total_requests\":1,\"total_records_fetched\":0,\"total_elapsed_seconds\":0.24,\"average_seconds_per_request\":0.24} {\"correlation_id\":\"e1af9830-ccba-44db-a1a0-d9ac3ec0da4d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612360,\"participants_processed\":3,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"e1af9830-ccba-44db-a1a0-d9ac3ec0da4d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1310,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612360} {\"correlation_id\":\"e1af9830-ccba-44db-a1a0-d9ac3ec0da4d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1310,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612360,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"e1af9830-ccba-44db-a1a0-d9ac3ec0da4d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1310,\"provider\":\"google\",\"refreshToken\":\"e34818922c2830a660813a63f6169a4a9a992ae2cccd7dc8dd7796cfdb470ef1\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612360,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"e1af9830-ccba-44db-a1a0-d9ac3ec0da4d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612340,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"eb3baeb0-9f99-4e3d-98f1-fdc0d69023fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612340} {\"correlation_id\":\"eb3baeb0-9f99-4e3d-98f1-fdc0d69023fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612340,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"eb3baeb0-9f99-4e3d-98f1-fdc0d69023fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612340,\"participants\":[{\"id\":999516,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null},{\"id\":999517,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999518,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":999519,\"user_id\":null,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"eb3baeb0-9f99-4e3d-98f1-fdc0d69023fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"eb3baeb0-9f99-4e3d-98f1-fdc0d69023fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"eb3baeb0-9f99-4e3d-98f1-fdc0d69023fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"eb3baeb0-9f99-4e3d-98f1-fdc0d69023fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"eb3baeb0-9f99-4e3d-98f1-fdc0d69023fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"eb3baeb0-9f99-4e3d-98f1-fdc0d69023fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612340,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"eb3baeb0-9f99-4e3d-98f1-fdc0d69023fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1310,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Bad Request\"}} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1310,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1333,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1333,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1333,\"provider\":\"google\",\"refreshToken\":\"6c902986546d8e8da1dc539b046cdc1d458f519acc972e5b5f1d6a1a295165e0\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [Hubspot] Pagination completed {\"team_id\":2,\"endpoint\":\"https://api.hubapi.com/crm/v3/objects/contact/search\",\"total_requests\":1,\"total_records_fetched\":0,\"total_elapsed_seconds\":0.22,\"average_seconds_per_request\":0.22} {\"correlation_id\":\"eb3baeb0-9f99-4e3d-98f1-fdc0d69023fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612340,\"participants_processed\":4,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"eb3baeb0-9f99-4e3d-98f1-fdc0d69023fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612340} {\"correlation_id\":\"eb3baeb0-9f99-4e3d-98f1-fdc0d69023fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612340,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"eb3baeb0-9f99-4e3d-98f1-fdc0d69023fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612340,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"eb3baeb0-9f99-4e3d-98f1-fdc0d69023fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612339,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"68ce2e50-cf66-4aab-9ce0-ab242e6027c4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612339} {\"correlation_id\":\"68ce2e50-cf66-4aab-9ce0-ab242e6027c4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612339,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"68ce2e50-cf66-4aab-9ce0-ab242e6027c4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612339,\"participants\":[{\"id\":999514,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null},{\"id\":999515,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999540,\"user_id\":null,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"68ce2e50-cf66-4aab-9ce0-ab242e6027c4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"68ce2e50-cf66-4aab-9ce0-ab242e6027c4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"68ce2e50-cf66-4aab-9ce0-ab242e6027c4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"68ce2e50-cf66-4aab-9ce0-ab242e6027c4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"68ce2e50-cf66-4aab-9ce0-ab242e6027c4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"68ce2e50-cf66-4aab-9ce0-ab242e6027c4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612339,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"68ce2e50-cf66-4aab-9ce0-ab242e6027c4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612339,\"participants_processed\":3,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"68ce2e50-cf66-4aab-9ce0-ab242e6027c4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1333,\"provider\":\"google\",\"responseBody\":{\"error\":\"unauthorized_client\",\"error_description\":\"Unauthorized\"}} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612339} {\"correlation_id\":\"68ce2e50-cf66-4aab-9ce0-ab242e6027c4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1333,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612339,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"68ce2e50-cf66-4aab-9ce0-ab242e6027c4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612339,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"68ce2e50-cf66-4aab-9ce0-ab242e6027c4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1368,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1368,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1368,\"provider\":\"google\",\"refreshToken\":\"d2f128898ff8543bd16b69cfae37896ab85119b0f5ed2b431d739593bb600333\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612336,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"ffc0ea4a-744d-49a4-a9ad-736fbda03384\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612336} {\"correlation_id\":\"ffc0ea4a-744d-49a4-a9ad-736fbda03384\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612336,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"ffc0ea4a-744d-49a4-a9ad-736fbda03384\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612336,\"participants\":[{\"id\":999508,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null},{\"id\":999509,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999512,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":999513,\"user_id\":null,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"ffc0ea4a-744d-49a4-a9ad-736fbda03384\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"ffc0ea4a-744d-49a4-a9ad-736fbda03384\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"ffc0ea4a-744d-49a4-a9ad-736fbda03384\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"ffc0ea4a-744d-49a4-a9ad-736fbda03384\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"ffc0ea4a-744d-49a4-a9ad-736fbda03384\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"ffc0ea4a-744d-49a4-a9ad-736fbda03384\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612336,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"ffc0ea4a-744d-49a4-a9ad-736fbda03384\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612336,\"participants_processed\":4,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"ffc0ea4a-744d-49a4-a9ad-736fbda03384\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612336} {\"correlation_id\":\"ffc0ea4a-744d-49a4-a9ad-736fbda03384\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612336,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"ffc0ea4a-744d-49a4-a9ad-736fbda03384\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612336,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"ffc0ea4a-744d-49a4-a9ad-736fbda03384\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612183,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36}} {\"correlation_id\":\"48880be8-e032-41b2-948e-c4ec99247548\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612183} {\"correlation_id\":\"48880be8-e032-41b2-948e-c4ec99247548\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612183,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"48880be8-e032-41b2-948e-c4ec99247548\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612183,\"participants\":[{\"id\":999227,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null},{\"id\":999228,\"user_id\":261,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"48880be8-e032-41b2-948e-c4ec99247548\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"48880be8-e032-41b2-948e-c4ec99247548\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"48880be8-e032-41b2-948e-c4ec99247548\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"48880be8-e032-41b2-948e-c4ec99247548\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"48880be8-e032-41b2-948e-c4ec99247548\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"48880be8-e032-41b2-948e-c4ec99247548\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612183,\"team_id\":2,\"email\":\"adelina.petrova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"48880be8-e032-41b2-948e-c4ec99247548\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612183,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"48880be8-e032-41b2-948e-c4ec99247548\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612183} {\"correlation_id\":\"48880be8-e032-41b2-948e-c4ec99247548\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612183,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"48880be8-e032-41b2-948e-c4ec99247548\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612183,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36} {\"correlation_id\":\"48880be8-e032-41b2-948e-c4ec99247548\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612182,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36}} {\"correlation_id\":\"3858f13d-426c-474e-98cf-11576b98e5dd\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612182} {\"correlation_id\":\"3858f13d-426c-474e-98cf-11576b98e5dd\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612182,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"3858f13d-426c-474e-98cf-11576b98e5dd\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612182,\"participants\":[{\"id\":999225,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null},{\"id\":999226,\"user_id\":261,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"3858f13d-426c-474e-98cf-11576b98e5dd\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"3858f13d-426c-474e-98cf-11576b98e5dd\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"3858f13d-426c-474e-98cf-11576b98e5dd\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"3858f13d-426c-474e-98cf-11576b98e5dd\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"3858f13d-426c-474e-98cf-11576b98e5dd\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"3858f13d-426c-474e-98cf-11576b98e5dd\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612182,\"team_id\":2,\"email\":\"adelina.petrova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"3858f13d-426c-474e-98cf-11576b98e5dd\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612182,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"3858f13d-426c-474e-98cf-11576b98e5dd\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612182} {\"correlation_id\":\"3858f13d-426c-474e-98cf-11576b98e5dd\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612182,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"3858f13d-426c-474e-98cf-11576b98e5dd\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612182,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36} {\"correlation_id\":\"3858f13d-426c-474e-98cf-11576b98e5dd\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612181,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36}} {\"correlation_id\":\"33fd8677-950a-4069-9642-511743b8b619\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612181} {\"correlation_id\":\"33fd8677-950a-4069-9642-511743b8b619\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612181,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"33fd8677-950a-4069-9642-511743b8b619\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612181,\"participants\":[{\"id\":999223,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null},{\"id\":999224,\"user_id\":261,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"33fd8677-950a-4069-9642-511743b8b619\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"33fd8677-950a-4069-9642-511743b8b619\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"33fd8677-950a-4069-9642-511743b8b619\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"33fd8677-950a-4069-9642-511743b8b619\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"33fd8677-950a-4069-9642-511743b8b619\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"33fd8677-950a-4069-9642-511743b8b619\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1368,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Bad Request\"}} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612181,\"team_id\":2,\"email\":\"adelina.petrova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"33fd8677-950a-4069-9642-511743b8b619\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1368,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612181,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"33fd8677-950a-4069-9642-511743b8b619\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612181} {\"correlation_id\":\"33fd8677-950a-4069-9642-511743b8b619\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612181,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"33fd8677-950a-4069-9642-511743b8b619\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612181,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36} {\"correlation_id\":\"33fd8677-950a-4069-9642-511743b8b619\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1365,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1365,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1365,\"provider\":\"google\",\"refreshToken\":\"7676e4a9afcd082b413248ab5ec6e487021fec6a9bdf315860a59cefad9caad8\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612180,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36}} {\"correlation_id\":\"dc184f1d-d7e2-4bc1-92a0-c572af69ba86\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612180} {\"correlation_id\":\"dc184f1d-d7e2-4bc1-92a0-c572af69ba86\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612180,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"dc184f1d-d7e2-4bc1-92a0-c572af69ba86\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612180,\"participants\":[{\"id\":999221,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null},{\"id\":999222,\"user_id\":261,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"dc184f1d-d7e2-4bc1-92a0-c572af69ba86\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"dc184f1d-d7e2-4bc1-92a0-c572af69ba86\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"dc184f1d-d7e2-4bc1-92a0-c572af69ba86\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"dc184f1d-d7e2-4bc1-92a0-c572af69ba86\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"dc184f1d-d7e2-4bc1-92a0-c572af69ba86\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"dc184f1d-d7e2-4bc1-92a0-c572af69ba86\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612180,\"team_id\":2,\"email\":\"adelina.petrova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"dc184f1d-d7e2-4bc1-92a0-c572af69ba86\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612180,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"dc184f1d-d7e2-4bc1-92a0-c572af69ba86\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612180} {\"correlation_id\":\"dc184f1d-d7e2-4bc1-92a0-c572af69ba86\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612180,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"dc184f1d-d7e2-4bc1-92a0-c572af69ba86\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612180,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36} {\"correlation_id\":\"dc184f1d-d7e2-4bc1-92a0-c572af69ba86\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":611455,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"8f14fe33-00cc-4910-996e-efa541fc5bb7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":611455} {\"correlation_id\":\"8f14fe33-00cc-4910-996e-efa541fc5bb7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":611455,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"8f14fe33-00cc-4910-996e-efa541fc5bb7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":611455,\"participants\":[{\"id\":997961,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":997962,\"user_id\":1460,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"8f14fe33-00cc-4910-996e-efa541fc5bb7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"8f14fe33-00cc-4910-996e-efa541fc5bb7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"8f14fe33-00cc-4910-996e-efa541fc5bb7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"8f14fe33-00cc-4910-996e-efa541fc5bb7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"8f14fe33-00cc-4910-996e-efa541fc5bb7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [Prospect match] Cache miss, calling the API {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"8f14fe33-00cc-4910-996e-efa541fc5bb7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1365,\"provider\":\"google\",\"responseBody\":{\"error\":\"unauthorized_client\",\"error_description\":\"Unauthorized\"}} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1365,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1364,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1364,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1364,\"provider\":\"google\",\"refreshToken\":\"dd5882ebce76e645292ce33ae74238abbb77c0a4ecc6a2bfe723cad82e72ba8e\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.INFO: [Hubspot] Failed to fetch contact {\"email\":\"support@staging.jiminny.com\",\"reason\":\"[404] Client error: `GET https://api.hubapi.com/crm/v3/objects/contacts/support%40staging.jiminny.com?properties=email%2Cfirstname%2Clastname%2Ccountry%2Cphone%2Cmobilephone%2Cjobtitle%2Chubspot_owner_id%2Cassociatedcompanyid%2Cphoto&archived=0&idProperty=email` resulted in a `404 Not Found` response\"} {\"correlation_id\":\"8f14fe33-00cc-4910-996e-efa541fc5bb7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [Prospect match] API returned empty result, caching the miss with empty prospect data {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"8f14fe33-00cc-4910-996e-efa541fc5bb7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1364,\"provider\":\"google\",\"responseBody\":{\"error\":\"unauthorized_client\",\"error_description\":\"Unauthorized\"}} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1364,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1370,\"provider\":\"office\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1370,\"provider\":\"office\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1370,\"provider\":\"office\",\"refreshToken\":\"b7ee8035306d0043cea6e00e7c4fe14f745e44074a1194db62a31cdf8b70af3e\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.INFO: [Hubspot] Pagination completed {\"team_id\":2,\"endpoint\":\"https://api.hubapi.com/crm/v3/objects/contact/search\",\"total_requests\":1,\"total_records_fetched\":0,\"total_elapsed_seconds\":0.21,\"average_seconds_per_request\":0.21} {\"correlation_id\":\"8f14fe33-00cc-4910-996e-efa541fc5bb7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"8f14fe33-00cc-4910-996e-efa541fc5bb7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [Prospect match] Cache miss {\"identifier_type\":\"domain\",\"identifier\":\"jiminny.com\",\"crm\":\"hubspot\"} {\"correlation_id\":\"8f14fe33-00cc-4910-996e-efa541fc5bb7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [Prospect match] Cache miss, calling the API {\"identifier_type\":\"domain\",\"identifier\":\"jiminny.com\"} {\"correlation_id\":\"8f14fe33-00cc-4910-996e-efa541fc5bb7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1370,\"provider\":\"office\",\"responseBody\":\"{\\\"error\\\":\\\"invalid_client\\\",\\\"error_description\\\":\\\"AADSTS7000215: Invalid client secret provided. Ensure the secret being sent in the request is the client secret value, not the client secret ID, for a secret added to app 'bbcbb2ef-6200-4fae-82bd-d81f5dd738da'. Trace ID: 329b429c-bad8-4f19-bace-40e48a111900 Correlation ID: 4f12c9af-e118-468c-8441-ebc325071d4b Timestamp: 2026-05-11 10:43:36Z\\\",\\\"error_codes\\\":[7000215],\\\"timestamp\\\":\\\"2026-05-11 10:43:36Z\\\",\\\"trace_id\\\":\\\"329b429c-bad8-4f19-bace-40e48a111900\\\",\\\"correlation_id\\\":\\\"4f12c9af-e118-468c-8441-ebc325071d4b\\\",\\\"error_uri\\\":\\\"https://login.microsoftonline.com/error?code=7000215\\\"}\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1370,\"provider\":\"office\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1202,\"provider\":\"office\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1202,\"provider\":\"office\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1202,\"provider\":\"office\",\"refreshToken\":\"b458799ccc29b21a6e2eb5260fdb63e49ccba21bf942a3973fb63799bd7f0afe\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.INFO: [HubSpot] importAccount {\"crm_provider_id\":\"749766179\",\"config_id\":2} {\"correlation_id\":\"8f14fe33-00cc-4910-996e-efa541fc5bb7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [HubSpot] CRM Search requested {\"request\":{\"filterGroups\":[{\"filters\":[{\"propertyName\":\"associations.company\",\"operator\":\"EQ\",\"value\":\"749766179\"},{\"propertyName\":\"dealstage\",\"operator\":\"NOT_IN\",\"values\":[\"closedwon\",\"4040964\",\"59247967\"]},{\"propertyName\":\"dealstage\",\"operator\":\"NOT_IN\",\"values\":[\"closedlost\",\"4040965\",\"59247968\"]}]}],\"sorts\":[{\"propertyName\":\"modifieddate\",\"direction\":\"DESCENDING\"}],\"properties\":[\"dealname\",\"amount\",\"hubspot_owner_id\",\"pipeline\",\"dealstage\",\"closedate\",\"deal_currency_code\"],\"limit\":200}} {\"correlation_id\":\"8f14fe33-00cc-4910-996e-efa541fc5bb7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:37] local.INFO: [Hubspot] Pagination completed {\"team_id\":2,\"endpoint\":\"https://api.hubapi.com/crm/v3/objects/deals/search\",\"total_requests\":1,\"total_records_fetched\":10,\"total_elapsed_seconds\":0.24,\"average_seconds_per_request\":0.24} {\"correlation_id\":\"8f14fe33-00cc-4910-996e-efa541fc5bb7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:37] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":611455,\"team_id\":2,\"email\":\"aneliya.angelova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"8f14fe33-00cc-4910-996e-efa541fc5bb7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:37] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":611455,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"8f14fe33-00cc-4910-996e-efa541fc5bb7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:37] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":611455} {\"correlation_id\":\"8f14fe33-00cc-4910-996e-efa541fc5bb7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:37] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":611455,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"8f14fe33-00cc-4910-996e-efa541fc5bb7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:37] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":611455,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"8f14fe33-00cc-4910-996e-efa541fc5bb7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:37] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":611451,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"7ea49a4a-ec21-4b93-b990-d1e79531b72a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:37] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":611451} {\"correlation_id\":\"7ea49a4a-ec21-4b93-b990-d1e79531b72a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:37] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":611451,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"7ea49a4a-ec21-4b93-b990-d1e79531b72a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:37] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":611451,\"participants\":[{\"id\":997955,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":997956,\"user_id\":18,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"7ea49a4a-ec21-4b93-b990-d1e79531b72a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:37] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"7ea49a4a-ec21-4b93-b990-d1e79531b72a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:37] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"7ea49a4a-ec21-4b93-b990-d1e79531b72a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:37] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"7ea49a4a-ec21-4b93-b990-d1e79531b72a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:37] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"7ea49a4a-ec21-4b93-b990-d1e79531b72a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:37] local.INFO: [Prospect match] Cache miss, calling the API {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"7ea49a4a-ec21-4b93-b990-d1e79531b72a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:38] local.INFO: [Hubspot] Failed to fetch contact {\"email\":\"support@staging.jiminny.com\",\"reason\":\"[404] Client error: `GET https://api.hubapi.com/crm/v3/objects/contacts/support%40staging.jiminny.com?properties=email%2Cfirstname%2Clastname%2Ccountry%2Cphone%2Cmobilephone%2Cjobtitle%2Chubspot_owner_id%2Cassociatedcompanyid%2Cphoto&archived=0&idProperty=email` resulted in a `404 Not Found` response\"} {\"correlation_id\":\"7ea49a4a-ec21-4b93-b990-d1e79531b72a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:38] local.INFO: [Prospect match] API returned empty result, caching the miss with empty prospect data {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"7ea49a4a-ec21-4b93-b990-d1e79531b72a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:38] local.INFO: [Hubspot] Pagination completed {\"team_id\":2,\"endpoint\":\"https://api.hubapi.com/crm/v3/objects/contact/search\",\"total_requests\":1,\"total_records_fetched\":0,\"total_elapsed_seconds\":0.22,\"average_seconds_per_request\":0.22} {\"correlation_id\":\"7ea49a4a-ec21-4b93-b990-d1e79531b72a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:38] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"7ea49a4a-ec21-4b93-b990-d1e79531b72a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:38] local.INFO: [Prospect match] Cache miss {\"identifier_type\":\"domain\",\"identifier\":\"jiminny.com\",\"crm\":\"hubspot\"} {\"correlation_id\":\"7ea49a4a-ec21-4b93-b990-d1e79531b72a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:38] local.INFO: [Prospect match] Cache miss, calling the API {\"identifier_type\":\"domain\",\"identifier\":\"jiminny.com\"} {\"correlation_id\":\"7ea49a4a-ec21-4b93-b990-d1e79531b72a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:38] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1202,\"provider\":\"office\",\"responseBody\":\"{\\\"error\\\":\\\"invalid_client\\\",\\\"error_description\\\":\\\"AADSTS7000215: Invalid client secret provided. Ensure the secret being sent in the request is the client secret value, not the client secret ID, for a secret added to app 'bbcbb2ef-6200-4fae-82bd-d81f5dd738da'. Trace ID: e8a4c6ea-4ab7-4915-b7a3-93868da51100 Correlation ID: e670afc0-d33c-4205-924c-76bda5c3d01d Timestamp: 2026-05-11 10:43:38Z\\\",\\\"error_codes\\\":[7000215],\\\"timestamp\\\":\\\"2026-05-11 10:43:38Z\\\",\\\"trace_id\\\":\\\"e8a4c6ea-4ab7-4915-b7a3-93868da51100\\\",\\\"correlation_id\\\":\\\"e670afc0-d33c-4205-924c-76bda5c3d01d\\\",\\\"error_uri\\\":\\\"https://login.microsoftonline.com/error?code=7000215\\\"}\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:38] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:38] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1202,\"provider\":\"office\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:38] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1502,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:38] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1502,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:38] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:38] local.INFO: Calendar sync job dispatched {\"calendar_id\":501} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:38] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1300,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:38] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1300,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:38] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:38] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1300,\"provider\":\"google\",\"refreshToken\":\"4b811db0725fd9602a95943519a7da935e2a5065da7d9ebfcb170752e3e1ddb8\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:38] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1300,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Account has been deleted\"}} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:38] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:38] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1300,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:38] local.INFO: [HubSpot] importAccount {\"crm_provider_id\":\"749766179\",\"config_id\":2} {\"correlation_id\":\"7ea49a4a-ec21-4b93-b990-d1e79531b72a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:38] local.INFO: [HubSpot] CRM Search requested {\"request\":{\"filterGroups\":[{\"filters\":[{\"propertyName\":\"associations.company\",\"operator\":\"EQ\",\"value\":\"749766179\"},{\"propertyName\":\"dealstage\",\"operator\":\"NOT_IN\",\"values\":[\"closedwon\",\"4040964\",\"59247967\"]},{\"propertyName\":\"dealstage\",\"operator\":\"NOT_IN\",\"values\":[\"closedlost\",\"4040965\",\"59247968\"]}]}],\"sorts\":[{\"propertyName\":\"modifieddate\",\"direction\":\"DESCENDING\"}],\"properties\":[\"dealname\",\"amount\",\"hubspot_owner_id\",\"pipeline\",\"dealstage\",\"closedate\",\"deal_currency_code\"],\"limit\":200}} {\"correlation_id\":\"7ea49a4a-ec21-4b93-b990-d1e79531b72a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:38] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1409,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:38] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1409,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:38] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:38] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1409,\"provider\":\"google\",\"refreshToken\":\"e2a3f2d06894894eed1ee87d9db1ace77d4d42ee6e1288a8940ad2c10333b0c4\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:38] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1409,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Bad Request\"}} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:38] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:38] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1409,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:39] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1352,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:39] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1352,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:39] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:39] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1352,\"provider\":\"google\",\"refreshToken\":\"dd4b16b00fdc1216da6b717c02338c073636e29162826b2de6db3f064fc029eb\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:39] local.INFO: [Hubspot] Pagination completed {\"team_id\":2,\"endpoint\":\"https://api.hubapi.com/crm/v3/objects/deals/search\",\"total_requests\":1,\"total_records_fetched\":10,\"total_elapsed_seconds\":0.28,\"average_seconds_per_request\":0.28} {\"correlation_id\":\"7ea49a4a-ec21-4b93-b990-d1e79531b72a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1352,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Token has been expired or revoked.\"}} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:39] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:39] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1352,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:39] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1296,\"provider\":\"office\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:39] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1296,\"provider\":\"office\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:39] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:39] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1296,\"provider\":\"office\",\"refreshToken\":\"011ae723c9d800c674e0b4be76f49fc046dac7d501b66c59ef0d9549cfa56ae5\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:39] local.INFO: [ EsUpdateProcessManager ] Finished updating entities in ES {\"worker\":\"\",\"peak_memory\":\"99.73 MB\",\"elapsed_seconds\":0.9,\"update_target\":\"activities\",\"should_iterate_again\":false} {\"correlation_id\":\"4ad05333-9afb-492e-9f0f-b2909ac45b32\",\"trace_id\":\"3d8feb24-b173-4158-b0a4-4cf33af85066\"}\n[2026-05-11 10:43:39] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":611451,\"team_id\":2,\"email\":\"veselin.kulov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"7ea49a4a-ec21-4b93-b990-d1e79531b72a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":611451,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"7ea49a4a-ec21-4b93-b990-d1e79531b72a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":611451} {\"correlation_id\":\"7ea49a4a-ec21-4b93-b990-d1e79531b72a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":611451,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"7ea49a4a-ec21-4b93-b990-d1e79531b72a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":611451,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"7ea49a4a-ec21-4b93-b990-d1e79531b72a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":611087,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"02dbf6fa-a774-4090-af49-cabec99363d4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":611087} {\"correlation_id\":\"02dbf6fa-a774-4090-af49-cabec99363d4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":611087,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"02dbf6fa-a774-4090-af49-cabec99363d4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":611087,\"participants\":[{\"id\":997368,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":997369,\"user_id\":18,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"02dbf6fa-a774-4090-af49-cabec99363d4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"02dbf6fa-a774-4090-af49-cabec99363d4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"02dbf6fa-a774-4090-af49-cabec99363d4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"02dbf6fa-a774-4090-af49-cabec99363d4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"02dbf6fa-a774-4090-af49-cabec99363d4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"02dbf6fa-a774-4090-af49-cabec99363d4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"02dbf6fa-a774-4090-af49-cabec99363d4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"02dbf6fa-a774-4090-af49-cabec99363d4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":611087,\"team_id\":2,\"email\":\"veselin.kulov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"02dbf6fa-a774-4090-af49-cabec99363d4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":611087,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"02dbf6fa-a774-4090-af49-cabec99363d4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":611087} {\"correlation_id\":\"02dbf6fa-a774-4090-af49-cabec99363d4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":611087,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"02dbf6fa-a774-4090-af49-cabec99363d4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":611087,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"02dbf6fa-a774-4090-af49-cabec99363d4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1296,\"provider\":\"office\",\"responseBody\":\"{\\\"error\\\":\\\"invalid_client\\\",\\\"error_description\\\":\\\"AADSTS7000215: Invalid client secret provided. Ensure the secret being sent in the request is the client secret value, not the client secret ID, for a secret added to app 'bbcbb2ef-6200-4fae-82bd-d81f5dd738da'. Trace ID: 4ff43bd2-d427-4049-964c-57fbcad91a00 Correlation ID: 6e92626f-a36d-4c41-ae7c-3b7296df0110 Timestamp: 2026-05-11 10:43:39Z\\\",\\\"error_codes\\\":[7000215],\\\"timestamp\\\":\\\"2026-05-11 10:43:39Z\\\",\\\"trace_id\\\":\\\"4ff43bd2-d427-4049-964c-57fbcad91a00\\\",\\\"correlation_id\\\":\\\"6e92626f-a36d-4c41-ae7c-3b7296df0110\\\",\\\"error_uri\\\":\\\"https://login.microsoftonline.com/error?code=7000215\\\"}\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:39] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:39] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1296,\"provider\":\"office\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:39] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":611076,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"ae1d456f-4b46-4c79-bb7f-bc2e95d48c9f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":611076} {\"correlation_id\":\"ae1d456f-4b46-4c79-bb7f-bc2e95d48c9f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":611076,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"ae1d456f-4b46-4c79-bb7f-bc2e95d48c9f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":611076,\"participants\":[{\"id\":997346,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":997347,\"user_id\":18,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"ae1d456f-4b46-4c79-bb7f-bc2e95d48c9f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":391,\"provider\":\"office\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:39] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":391,\"provider\":\"office\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:39] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:39] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":391,\"provider\":\"office\",\"refreshToken\":\"00045eebae0f39b34887c6d53f92ae78064f7145e1f4b67754aebd03cfb2d881\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:39] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"ae1d456f-4b46-4c79-bb7f-bc2e95d48c9f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"ae1d456f-4b46-4c79-bb7f-bc2e95d48c9f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"ae1d456f-4b46-4c79-bb7f-bc2e95d48c9f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"ae1d456f-4b46-4c79-bb7f-bc2e95d48c9f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"ae1d456f-4b46-4c79-bb7f-bc2e95d48c9f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"ae1d456f-4b46-4c79-bb7f-bc2e95d48c9f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"ae1d456f-4b46-4c79-bb7f-bc2e95d48c9f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":611076,\"team_id\":2,\"email\":\"veselin.kulov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"ae1d456f-4b46-4c79-bb7f-bc2e95d48c9f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":611076,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"ae1d456f-4b46-4c79-bb7f-bc2e95d48c9f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":611076} {\"correlation_id\":\"ae1d456f-4b46-4c79-bb7f-bc2e95d48c9f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":611076,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"ae1d456f-4b46-4c79-bb7f-bc2e95d48c9f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":611076,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"ae1d456f-4b46-4c79-bb7f-bc2e95d48c9f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1502,\"provider\":\"google\"} {\"correlation_id\":\"0fdd7dce-04e2-4696-ba87-8089a739c81c\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1502,\"provider\":\"google\"} {\"correlation_id\":\"0fdd7dce-04e2-4696-ba87-8089a739c81c\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:40] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"0fdd7dce-04e2-4696-ba87-8089a739c81c\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:40] local.INFO: [Calendar] Processing sync {\"calendarId\":\"a33076c1-8d97-431a-99f0-85c9524e118b\",\"from\":null,\"to\":null,\"delta\":\"CIiFh8TP44kDEIiFh8TP44kDGAUgkZvkzgIokZvkzgI=\",\"last_sync\":\"2024-12-09 07:12:53\",\"dateMode\":\"daily\"} {\"correlation_id\":\"0fdd7dce-04e2-4696-ba87-8089a739c81c\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:40] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"integration-app\",\"crm_owner\":1695,\"team_id\":3143} {\"correlation_id\":\"0fdd7dce-04e2-4696-ba87-8089a739c81c\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1502,\"provider\":\"google\"} {\"correlation_id\":\"0fdd7dce-04e2-4696-ba87-8089a739c81c\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1502,\"provider\":\"google\"} {\"correlation_id\":\"0fdd7dce-04e2-4696-ba87-8089a739c81c\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:40] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"0fdd7dce-04e2-4696-ba87-8089a739c81c\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":610935,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"9f5579a5-e89f-4074-9843-d5c1f123878d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610935} {\"correlation_id\":\"9f5579a5-e89f-4074-9843-d5c1f123878d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610935,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"9f5579a5-e89f-4074-9843-d5c1f123878d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":610935,\"participants\":[{\"id\":997141,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":997142,\"user_id\":18,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"9f5579a5-e89f-4074-9843-d5c1f123878d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"9f5579a5-e89f-4074-9843-d5c1f123878d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"9f5579a5-e89f-4074-9843-d5c1f123878d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"9f5579a5-e89f-4074-9843-d5c1f123878d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"9f5579a5-e89f-4074-9843-d5c1f123878d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"9f5579a5-e89f-4074-9843-d5c1f123878d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"9f5579a5-e89f-4074-9843-d5c1f123878d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"9f5579a5-e89f-4074-9843-d5c1f123878d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":610935,\"team_id\":2,\"email\":\"veselin.kulov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"9f5579a5-e89f-4074-9843-d5c1f123878d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":610935,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"9f5579a5-e89f-4074-9843-d5c1f123878d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610935} {\"correlation_id\":\"9f5579a5-e89f-4074-9843-d5c1f123878d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610935,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"9f5579a5-e89f-4074-9843-d5c1f123878d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":610935,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"9f5579a5-e89f-4074-9843-d5c1f123878d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":610915,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"ee1e1cdc-f0b6-4353-b761-ada15282a954\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610915} {\"correlation_id\":\"ee1e1cdc-f0b6-4353-b761-ada15282a954\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610915,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"ee1e1cdc-f0b6-4353-b761-ada15282a954\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":610915,\"participants\":[{\"id\":997104,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":997105,\"user_id\":18,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"ee1e1cdc-f0b6-4353-b761-ada15282a954\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"ee1e1cdc-f0b6-4353-b761-ada15282a954\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"ee1e1cdc-f0b6-4353-b761-ada15282a954\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"ee1e1cdc-f0b6-4353-b761-ada15282a954\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"ee1e1cdc-f0b6-4353-b761-ada15282a954\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"ee1e1cdc-f0b6-4353-b761-ada15282a954\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"ee1e1cdc-f0b6-4353-b761-ada15282a954\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"ee1e1cdc-f0b6-4353-b761-ada15282a954\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":610915,\"team_id\":2,\"email\":\"veselin.kulov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"ee1e1cdc-f0b6-4353-b761-ada15282a954\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":610915,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"ee1e1cdc-f0b6-4353-b761-ada15282a954\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610915} {\"correlation_id\":\"ee1e1cdc-f0b6-4353-b761-ada15282a954\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610915,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"ee1e1cdc-f0b6-4353-b761-ada15282a954\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":610915,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"ee1e1cdc-f0b6-4353-b761-ada15282a954\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":610900,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"46958b32-a07f-4aa9-a052-c6a1e3bf70b2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610900} {\"correlation_id\":\"46958b32-a07f-4aa9-a052-c6a1e3bf70b2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610900,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"46958b32-a07f-4aa9-a052-c6a1e3bf70b2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":610900,\"participants\":[{\"id\":997081,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":997082,\"user_id\":18,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"46958b32-a07f-4aa9-a052-c6a1e3bf70b2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"46958b32-a07f-4aa9-a052-c6a1e3bf70b2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"46958b32-a07f-4aa9-a052-c6a1e3bf70b2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"46958b32-a07f-4aa9-a052-c6a1e3bf70b2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"46958b32-a07f-4aa9-a052-c6a1e3bf70b2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"46958b32-a07f-4aa9-a052-c6a1e3bf70b2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"46958b32-a07f-4aa9-a052-c6a1e3bf70b2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"46958b32-a07f-4aa9-a052-c6a1e3bf70b2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":610900,\"team_id\":2,\"email\":\"veselin.kulov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"46958b32-a07f-4aa9-a052-c6a1e3bf70b2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":610900,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"46958b32-a07f-4aa9-a052-c6a1e3bf70b2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610900} {\"correlation_id\":\"46958b32-a07f-4aa9-a052-c6a1e3bf70b2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610900,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"46958b32-a07f-4aa9-a052-c6a1e3bf70b2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":610900,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"46958b32-a07f-4aa9-a052-c6a1e3bf70b2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":610885,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"b9dc6bd0-a276-4d4c-ad70-8cd444119160\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610885} {\"correlation_id\":\"b9dc6bd0-a276-4d4c-ad70-8cd444119160\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610885,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"b9dc6bd0-a276-4d4c-ad70-8cd444119160\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":610885,\"participants\":[{\"id\":997051,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":997052,\"user_id\":18,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"b9dc6bd0-a276-4d4c-ad70-8cd444119160\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"b9dc6bd0-a276-4d4c-ad70-8cd444119160\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"b9dc6bd0-a276-4d4c-ad70-8cd444119160\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"b9dc6bd0-a276-4d4c-ad70-8cd444119160\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"b9dc6bd0-a276-4d4c-ad70-8cd444119160\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"b9dc6bd0-a276-4d4c-ad70-8cd444119160\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"b9dc6bd0-a276-4d4c-ad70-8cd444119160\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"b9dc6bd0-a276-4d4c-ad70-8cd444119160\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":610885,\"team_id\":2,\"email\":\"veselin.kulov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"b9dc6bd0-a276-4d4c-ad70-8cd444119160\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":610885,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"b9dc6bd0-a276-4d4c-ad70-8cd444119160\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610885} {\"correlation_id\":\"b9dc6bd0-a276-4d4c-ad70-8cd444119160\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610885,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"b9dc6bd0-a276-4d4c-ad70-8cd444119160\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":610885,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"b9dc6bd0-a276-4d4c-ad70-8cd444119160\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":610878,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"117202f1-4432-416d-922c-d7cd0f2ae201\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610878} {\"correlation_id\":\"117202f1-4432-416d-922c-d7cd0f2ae201\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610878,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"117202f1-4432-416d-922c-d7cd0f2ae201\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":610878,\"participants\":[{\"id\":997035,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":997036,\"user_id\":18,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"117202f1-4432-416d-922c-d7cd0f2ae201\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":391,\"provider\":\"office\",\"responseBody\":\"{\\\"error\\\":\\\"invalid_client\\\",\\\"error_description\\\":\\\"AADSTS7000215: Invalid client secret provided. Ensure the secret being sent in the request is the client secret value, not the client secret ID, for a secret added to app 'bbcbb2ef-6200-4fae-82bd-d81f5dd738da'. Trace ID: 72b211a8-6a7c-4954-b720-f1a47e981b00 Correlation ID: 5f9781fe-b28a-44ce-94e3-42c683cdfd60 Timestamp: 2026-05-11 10:43:40Z\\\",\\\"error_codes\\\":[7000215],\\\"timestamp\\\":\\\"2026-05-11 10:43:40Z\\\",\\\"trace_id\\\":\\\"72b211a8-6a7c-4954-b720-f1a47e981b00\\\",\\\"correlation_id\\\":\\\"5f9781fe-b28a-44ce-94e3-42c683cdfd60\\\",\\\"error_uri\\\":\\\"https://login.microsoftonline.com/error?code=7000215\\\"}\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:40] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":391,\"provider\":\"office\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"117202f1-4432-416d-922c-d7cd0f2ae201\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"117202f1-4432-416d-922c-d7cd0f2ae201\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"117202f1-4432-416d-922c-d7cd0f2ae201\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"117202f1-4432-416d-922c-d7cd0f2ae201\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1271,\"provider\":\"office\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1271,\"provider\":\"office\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:40] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"117202f1-4432-416d-922c-d7cd0f2ae201\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1271,\"provider\":\"office\",\"refreshToken\":\"118cde2c06993147b07ccaec4cbcd5026a819dea6c71081166a492933e392afb\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"117202f1-4432-416d-922c-d7cd0f2ae201\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"117202f1-4432-416d-922c-d7cd0f2ae201\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":610878,\"team_id\":2,\"email\":\"veselin.kulov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"117202f1-4432-416d-922c-d7cd0f2ae201\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":610878,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"117202f1-4432-416d-922c-d7cd0f2ae201\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610878} {\"correlation_id\":\"117202f1-4432-416d-922c-d7cd0f2ae201\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610878,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"117202f1-4432-416d-922c-d7cd0f2ae201\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":610878,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"117202f1-4432-416d-922c-d7cd0f2ae201\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Google Calendar] Failed to watch channel for calendar {\"calendarId\":\"a33076c1-8d97-431a-99f0-85c9524e118b\",\"code\":400,\"reason\":\"{\n \\\"error\\\": {\n \\\"errors\\\": [\n {\n \\\"domain\\\": \\\"global\\\",\n \\\"reason\\\": \\\"push.webhookUrlNotHttps\\\",\n \\\"message\\\": \\\"WebHook callback must be HTTPS: /webhook/calendar/google?resourceType=event\\\"\n }\n ],\n \\\"code\\\": 400,\n \\\"message\\\": \\\"WebHook callback must be HTTPS: /webhook/calendar/google?resourceType=event\\\"\n }\n}\"} {\"correlation_id\":\"0fdd7dce-04e2-4696-ba87-8089a739c81c\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:40] local.WARNING: [Calendar] Sync failed {\"calendarId\":\"a33076c1-8d97-431a-99f0-85c9524e118b\",\"code\":400,\"reason\":\"{\n \\\"error\\\": {\n \\\"errors\\\": [\n {\n \\\"domain\\\": \\\"global\\\",\n \\\"reason\\\": \\\"push.webhookUrlNotHttps\\\",\n \\\"message\\\": \\\"WebHook callback must be HTTPS: /webhook/calendar/google?resourceType=event\\\"\n }\n ],\n \\\"code\\\": 400,\n \\\"message\\\": \\\"WebHook callback must be HTTPS: /webhook/calendar/google?resourceType=event\\\"\n }\n}\"} {\"correlation_id\":\"0fdd7dce-04e2-4696-ba87-8089a739c81c\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":610874,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"6264f69a-8461-4d85-9725-ef4c30fb5d35\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610874} {\"correlation_id\":\"6264f69a-8461-4d85-9725-ef4c30fb5d35\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610874,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"6264f69a-8461-4d85-9725-ef4c30fb5d35\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":610874,\"participants\":[{\"id\":997025,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":997026,\"user_id\":18,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"6264f69a-8461-4d85-9725-ef4c30fb5d35\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"6264f69a-8461-4d85-9725-ef4c30fb5d35\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"6264f69a-8461-4d85-9725-ef4c30fb5d35\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"6264f69a-8461-4d85-9725-ef4c30fb5d35\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"6264f69a-8461-4d85-9725-ef4c30fb5d35\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"6264f69a-8461-4d85-9725-ef4c30fb5d35\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"6264f69a-8461-4d85-9725-ef4c30fb5d35\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"6264f69a-8461-4d85-9725-ef4c30fb5d35\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":610874,\"team_id\":2,\"email\":\"veselin.kulov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"6264f69a-8461-4d85-9725-ef4c30fb5d35\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":610874,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"6264f69a-8461-4d85-9725-ef4c30fb5d35\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610874} {\"correlation_id\":\"6264f69a-8461-4d85-9725-ef4c30fb5d35\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610874,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"6264f69a-8461-4d85-9725-ef4c30fb5d35\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":610874,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"6264f69a-8461-4d85-9725-ef4c30fb5d35\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":610867,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"978698d8-e33a-4d4a-b374-04919cae6ac2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610867} {\"correlation_id\":\"978698d8-e33a-4d4a-b374-04919cae6ac2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610867,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"978698d8-e33a-4d4a-b374-04919cae6ac2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":610867,\"participants\":[{\"id\":997011,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":997012,\"user_id\":18,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"978698d8-e33a-4d4a-b374-04919cae6ac2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"978698d8-e33a-4d4a-b374-04919cae6ac2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"978698d8-e33a-4d4a-b374-04919cae6ac2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"978698d8-e33a-4d4a-b374-04919cae6ac2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"978698d8-e33a-4d4a-b374-04919cae6ac2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"978698d8-e33a-4d4a-b374-04919cae6ac2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"978698d8-e33a-4d4a-b374-04919cae6ac2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"978698d8-e33a-4d4a-b374-04919cae6ac2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":610867,\"team_id\":2,\"email\":\"veselin.kulov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"978698d8-e33a-4d4a-b374-04919cae6ac2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":610867,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"978698d8-e33a-4d4a-b374-04919cae6ac2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610867} {\"correlation_id\":\"978698d8-e33a-4d4a-b374-04919cae6ac2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610867,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"978698d8-e33a-4d4a-b374-04919cae6ac2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":610867,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"978698d8-e33a-4d4a-b374-04919cae6ac2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":610764,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"3e294e41-3a19-4afe-87b3-4f7f0336107c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610764} {\"correlation_id\":\"3e294e41-3a19-4afe-87b3-4f7f0336107c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610764,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"3e294e41-3a19-4afe-87b3-4f7f0336107c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":610764,\"participants\":[{\"id\":996951,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":996952,\"user_id\":18,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"3e294e41-3a19-4afe-87b3-4f7f0336107c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"3e294e41-3a19-4afe-87b3-4f7f0336107c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"3e294e41-3a19-4afe-87b3-4f7f0336107c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"3e294e41-3a19-4afe-87b3-4f7f0336107c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"3e294e41-3a19-4afe-87b3-4f7f0336107c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"3e294e41-3a19-4afe-87b3-4f7f0336107c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"3e294e41-3a19-4afe-87b3-4f7f0336107c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"3e294e41-3a19-4afe-87b3-4f7f0336107c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":610764,\"team_id\":2,\"email\":\"veselin.kulov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"3e294e41-3a19-4afe-87b3-4f7f0336107c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":610764,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"3e294e41-3a19-4afe-87b3-4f7f0336107c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610764} {\"correlation_id\":\"3e294e41-3a19-4afe-87b3-4f7f0336107c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610764,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"3e294e41-3a19-4afe-87b3-4f7f0336107c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":610764,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"3e294e41-3a19-4afe-87b3-4f7f0336107c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":610617,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"732b2bbb-ee7f-4bb2-85b1-e6478955544b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610617} {\"correlation_id\":\"732b2bbb-ee7f-4bb2-85b1-e6478955544b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610617,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"732b2bbb-ee7f-4bb2-85b1-e6478955544b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":610617,\"participants\":[{\"id\":996641,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":996642,\"user_id\":18,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"732b2bbb-ee7f-4bb2-85b1-e6478955544b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"732b2bbb-ee7f-4bb2-85b1-e6478955544b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"732b2bbb-ee7f-4bb2-85b1-e6478955544b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"732b2bbb-ee7f-4bb2-85b1-e6478955544b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"732b2bbb-ee7f-4bb2-85b1-e6478955544b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"732b2bbb-ee7f-4bb2-85b1-e6478955544b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"732b2bbb-ee7f-4bb2-85b1-e6478955544b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"732b2bbb-ee7f-4bb2-85b1-e6478955544b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":610617,\"team_id\":2,\"email\":\"veselin.kulov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"732b2bbb-ee7f-4bb2-85b1-e6478955544b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":610617,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"732b2bbb-ee7f-4bb2-85b1-e6478955544b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610617} {\"correlation_id\":\"732b2bbb-ee7f-4bb2-85b1-e6478955544b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610617,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"732b2bbb-ee7f-4bb2-85b1-e6478955544b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":610617,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"732b2bbb-ee7f-4bb2-85b1-e6478955544b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":610539,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"d94f0ac2-c007-4b7d-9fd3-27fc32f9b9a2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610539} {\"correlation_id\":\"d94f0ac2-c007-4b7d-9fd3-27fc32f9b9a2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610539,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"d94f0ac2-c007-4b7d-9fd3-27fc32f9b9a2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":610539,\"participants\":[{\"id\":996485,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":996486,\"user_id\":18,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"d94f0ac2-c007-4b7d-9fd3-27fc32f9b9a2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"d94f0ac2-c007-4b7d-9fd3-27fc32f9b9a2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"d94f0ac2-c007-4b7d-9fd3-27fc32f9b9a2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d94f0ac2-c007-4b7d-9fd3-27fc32f9b9a2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"d94f0ac2-c007-4b7d-9fd3-27fc32f9b9a2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"d94f0ac2-c007-4b7d-9fd3-27fc32f9b9a2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"d94f0ac2-c007-4b7d-9fd3-27fc32f9b9a2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"d94f0ac2-c007-4b7d-9fd3-27fc32f9b9a2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":610539,\"team_id\":2,\"email\":\"veselin.kulov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"d94f0ac2-c007-4b7d-9fd3-27fc32f9b9a2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":610539,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"d94f0ac2-c007-4b7d-9fd3-27fc32f9b9a2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610539} {\"correlation_id\":\"d94f0ac2-c007-4b7d-9fd3-27fc32f9b9a2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610539,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"d94f0ac2-c007-4b7d-9fd3-27fc32f9b9a2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":610539,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"d94f0ac2-c007-4b7d-9fd3-27fc32f9b9a2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":610528,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"c316e103-4e48-4f28-8bc6-206714fc00ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610528} {\"correlation_id\":\"c316e103-4e48-4f28-8bc6-206714fc00ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610528,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"c316e103-4e48-4f28-8bc6-206714fc00ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":610528,\"participants\":[{\"id\":996463,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":996464,\"user_id\":18,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"c316e103-4e48-4f28-8bc6-206714fc00ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"c316e103-4e48-4f28-8bc6-206714fc00ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"c316e103-4e48-4f28-8bc6-206714fc00ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"c316e103-4e48-4f28-8bc6-206714fc00ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"c316e103-4e48-4f28-8bc6-206714fc00ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"c316e103-4e48-4f28-8bc6-206714fc00ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"c316e103-4e48-4f28-8bc6-206714fc00ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"c316e103-4e48-4f28-8bc6-206714fc00ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":610528,\"team_id\":2,\"email\":\"veselin.kulov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"c316e103-4e48-4f28-8bc6-206714fc00ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":610528,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"c316e103-4e48-4f28-8bc6-206714fc00ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610528} {\"correlation_id\":\"c316e103-4e48-4f28-8bc6-206714fc00ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610528,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"c316e103-4e48-4f28-8bc6-206714fc00ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":610528,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"c316e103-4e48-4f28-8bc6-206714fc00ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":610506,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"f83f62c6-544e-4551-8bc2-99f405d022e6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610506} {\"correlation_id\":\"f83f62c6-544e-4551-8bc2-99f405d022e6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610506,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"f83f62c6-544e-4551-8bc2-99f405d022e6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":610506,\"participants\":[{\"id\":996419,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":996420,\"user_id\":18,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"f83f62c6-544e-4551-8bc2-99f405d022e6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"f83f62c6-544e-4551-8bc2-99f405d022e6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"f83f62c6-544e-4551-8bc2-99f405d022e6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"f83f62c6-544e-4551-8bc2-99f405d022e6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"f83f62c6-544e-4551-8bc2-99f405d022e6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"f83f62c6-544e-4551-8bc2-99f405d022e6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"f83f62c6-544e-4551-8bc2-99f405d022e6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"f83f62c6-544e-4551-8bc2-99f405d022e6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1271,\"provider\":\"office\",\"responseBody\":\"{\\\"error\\\":\\\"invalid_client\\\",\\\"error_description\\\":\\\"AADSTS7000215: Invalid client secret provided. Ensure the secret being sent in the request is the client secret value, not the client secret ID, for a secret added to app 'bbcbb2ef-6200-4fae-82bd-d81f5dd738da'. Trace ID: 8addf5ba-9879-4822-b2bb-e5002b0d2200 Correlation ID: feece826-abbe-4c27-85d0-0cddd6b33c3b Timestamp: 2026-05-11 10:43:41Z\\\",\\\"error_codes\\\":[7000215],\\\"timestamp\\\":\\\"2026-05-11 10:43:41Z\\\",\\\"trace_id\\\":\\\"8addf5ba-9879-4822-b2bb-e5002b0d2200\\\",\\\"correlation_id\\\":\\\"feece826-abbe-4c27-85d0-0cddd6b33c3b\\\",\\\"error_uri\\\":\\\"https://login.microsoftonline.com/error?code=7000215\\\"}\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1271,\"provider\":\"office\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":610506,\"team_id\":2,\"email\":\"veselin.kulov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"f83f62c6-544e-4551-8bc2-99f405d022e6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":610506,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"f83f62c6-544e-4551-8bc2-99f405d022e6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610506} {\"correlation_id\":\"f83f62c6-544e-4551-8bc2-99f405d022e6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610506,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"f83f62c6-544e-4551-8bc2-99f405d022e6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1351,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":610506,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"f83f62c6-544e-4551-8bc2-99f405d022e6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1351,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1351,\"provider\":\"google\",\"refreshToken\":\"4271d15b9e60a606439caddc68337f783e472c85b03dacff14d1b6dfded9051c\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":610497,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"854a8b20-3fc0-4ea6-a7c0-e0ddd599dd3b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610497} {\"correlation_id\":\"854a8b20-3fc0-4ea6-a7c0-e0ddd599dd3b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610497,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"854a8b20-3fc0-4ea6-a7c0-e0ddd599dd3b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":610497,\"participants\":[{\"id\":996401,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":996402,\"user_id\":18,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"854a8b20-3fc0-4ea6-a7c0-e0ddd599dd3b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1351,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Bad Request\"}} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1351,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"854a8b20-3fc0-4ea6-a7c0-e0ddd599dd3b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"854a8b20-3fc0-4ea6-a7c0-e0ddd599dd3b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"854a8b20-3fc0-4ea6-a7c0-e0ddd599dd3b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"854a8b20-3fc0-4ea6-a7c0-e0ddd599dd3b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1366,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1366,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"854a8b20-3fc0-4ea6-a7c0-e0ddd599dd3b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"854a8b20-3fc0-4ea6-a7c0-e0ddd599dd3b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1366,\"provider\":\"google\",\"refreshToken\":\"ae21385059b2eebfd43f68aecd56eccd702a1aabb6598f1f7ab594ed8af491b4\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"854a8b20-3fc0-4ea6-a7c0-e0ddd599dd3b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":610497,\"team_id\":2,\"email\":\"veselin.kulov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"854a8b20-3fc0-4ea6-a7c0-e0ddd599dd3b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":610497,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"854a8b20-3fc0-4ea6-a7c0-e0ddd599dd3b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610497} {\"correlation_id\":\"854a8b20-3fc0-4ea6-a7c0-e0ddd599dd3b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610497,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"854a8b20-3fc0-4ea6-a7c0-e0ddd599dd3b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":610497,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"854a8b20-3fc0-4ea6-a7c0-e0ddd599dd3b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":610490,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"bab4020c-8e0d-42eb-afdb-e6bab67d42c7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610490} {\"correlation_id\":\"bab4020c-8e0d-42eb-afdb-e6bab67d42c7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610490,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"bab4020c-8e0d-42eb-afdb-e6bab67d42c7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":610490,\"participants\":[{\"id\":996385,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":996386,\"user_id\":18,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"bab4020c-8e0d-42eb-afdb-e6bab67d42c7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"bab4020c-8e0d-42eb-afdb-e6bab67d42c7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"bab4020c-8e0d-42eb-afdb-e6bab67d42c7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"bab4020c-8e0d-42eb-afdb-e6bab67d42c7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"bab4020c-8e0d-42eb-afdb-e6bab67d42c7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"bab4020c-8e0d-42eb-afdb-e6bab67d42c7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"bab4020c-8e0d-42eb-afdb-e6bab67d42c7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"bab4020c-8e0d-42eb-afdb-e6bab67d42c7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":610490,\"team_id\":2,\"email\":\"veselin.kulov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"bab4020c-8e0d-42eb-afdb-e6bab67d42c7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":610490,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"bab4020c-8e0d-42eb-afdb-e6bab67d42c7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610490} {\"correlation_id\":\"bab4020c-8e0d-42eb-afdb-e6bab67d42c7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610490,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"bab4020c-8e0d-42eb-afdb-e6bab67d42c7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":610490,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"bab4020c-8e0d-42eb-afdb-e6bab67d42c7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1366,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Bad Request\"}} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1366,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":610470,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"55223d65-4d62-42ac-994b-ae4c8f27cbef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1115,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610470} {\"correlation_id\":\"55223d65-4d62-42ac-994b-ae4c8f27cbef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1115,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610470,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"55223d65-4d62-42ac-994b-ae4c8f27cbef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":610470,\"participants\":[{\"id\":996369,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":996370,\"user_id\":18,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"55223d65-4d62-42ac-994b-ae4c8f27cbef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: Calendar sync job dispatched {\"calendar_id\":378} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1421,\"provider\":\"office\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1421,\"provider\":\"office\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"55223d65-4d62-42ac-994b-ae4c8f27cbef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"55223d65-4d62-42ac-994b-ae4c8f27cbef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"55223d65-4d62-42ac-994b-ae4c8f27cbef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: Calendar sync job dispatched {\"calendar_id\":504} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"55223d65-4d62-42ac-994b-ae4c8f27cbef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"55223d65-4d62-42ac-994b-ae4c8f27cbef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"55223d65-4d62-42ac-994b-ae4c8f27cbef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"55223d65-4d62-42ac-994b-ae4c8f27cbef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.NOTICE: Calendar sync end {\"retrieved_calendars\":31,\"processed_calendars\":3} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"calendar:sync\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":610470,\"team_id\":2,\"email\":\"veselin.kulov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"55223d65-4d62-42ac-994b-ae4c8f27cbef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":610470,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"55223d65-4d62-42ac-994b-ae4c8f27cbef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610470} {\"correlation_id\":\"55223d65-4d62-42ac-994b-ae4c8f27cbef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610470,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"55223d65-4d62-42ac-994b-ae4c8f27cbef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":610470,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"55223d65-4d62-42ac-994b-ae4c8f27cbef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":610462,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"0108a9c2-fb98-43fc-ad71-084c53b40936\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610462} {\"correlation_id\":\"0108a9c2-fb98-43fc-ad71-084c53b40936\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610462,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"0108a9c2-fb98-43fc-ad71-084c53b40936\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":610462,\"participants\":[{\"id\":996353,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":996354,\"user_id\":18,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"0108a9c2-fb98-43fc-ad71-084c53b40936\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"0108a9c2-fb98-43fc-ad71-084c53b40936\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"0108a9c2-fb98-43fc-ad71-084c53b40936\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"0108a9c2-fb98-43fc-ad71-084c53b40936\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"0108a9c2-fb98-43fc-ad71-084c53b40936\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"0108a9c2-fb98-43fc-ad71-084c53b40936\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"0108a9c2-fb98-43fc-ad71-084c53b40936\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"0108a9c2-fb98-43fc-ad71-084c53b40936\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":610462,\"team_id\":2,\"email\":\"veselin.kulov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"0108a9c2-fb98-43fc-ad71-084c53b40936\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":610462,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"0108a9c2-fb98-43fc-ad71-084c53b40936\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610462} {\"correlation_id\":\"0108a9c2-fb98-43fc-ad71-084c53b40936\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610462,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"0108a9c2-fb98-43fc-ad71-084c53b40936\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":610462,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"0108a9c2-fb98-43fc-ad71-084c53b40936\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":610451,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"d1fa1d3a-52aa-496e-8905-56f7157299b1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610451} {\"correlation_id\":\"d1fa1d3a-52aa-496e-8905-56f7157299b1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610451,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"d1fa1d3a-52aa-496e-8905-56f7157299b1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":610451,\"participants\":[{\"id\":996340,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":996341,\"user_id\":18,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"d1fa1d3a-52aa-496e-8905-56f7157299b1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"d1fa1d3a-52aa-496e-8905-56f7157299b1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"d1fa1d3a-52aa-496e-8905-56f7157299b1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d1fa1d3a-52aa-496e-8905-56f7157299b1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"d1fa1d3a-52aa-496e-8905-56f7157299b1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"d1fa1d3a-52aa-496e-8905-56f7157299b1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"d1fa1d3a-52aa-496e-8905-56f7157299b1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"d1fa1d3a-52aa-496e-8905-56f7157299b1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":610451,\"team_id\":2,\"email\":\"veselin.kulov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"d1fa1d3a-52aa-496e-8905-56f7157299b1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":610451,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"d1fa1d3a-52aa-496e-8905-56f7157299b1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610451} {\"correlation_id\":\"d1fa1d3a-52aa-496e-8905-56f7157299b1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610451,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"d1fa1d3a-52aa-496e-8905-56f7157299b1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":610451,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"d1fa1d3a-52aa-496e-8905-56f7157299b1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":610438,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"da3f7962-a65d-4bfe-80bf-c33b4ccd36f5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610438} {\"correlation_id\":\"da3f7962-a65d-4bfe-80bf-c33b4ccd36f5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610438,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"da3f7962-a65d-4bfe-80bf-c33b4ccd36f5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":610438,\"participants\":[{\"id\":996320,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":996321,\"user_id\":18,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"da3f7962-a65d-4bfe-80bf-c33b4ccd36f5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"da3f7962-a65d-4bfe-80bf-c33b4ccd36f5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"da3f7962-a65d-4bfe-80bf-c33b4ccd36f5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"da3f7962-a65d-4bfe-80bf-c33b4ccd36f5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"da3f7962-a65d-4bfe-80bf-c33b4ccd36f5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"da3f7962-a65d-4bfe-80bf-c33b4ccd36f5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"da3f7962-a65d-4bfe-80bf-c33b4ccd36f5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"da3f7962-a65d-4bfe-80bf-c33b4ccd36f5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":610438,\"team_id\":2,\"email\":\"veselin.kulov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"da3f7962-a65d-4bfe-80bf-c33b4ccd36f5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":610438,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"da3f7962-a65d-4bfe-80bf-c33b4ccd36f5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610438} {\"correlation_id\":\"da3f7962-a65d-4bfe-80bf-c33b4ccd36f5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610438,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"da3f7962-a65d-4bfe-80bf-c33b4ccd36f5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":610438,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"da3f7962-a65d-4bfe-80bf-c33b4ccd36f5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":610426,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"da7b40c8-e233-4cf9-a133-31f12397ad10\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610426} {\"correlation_id\":\"da7b40c8-e233-4cf9-a133-31f12397ad10\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610426,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"da7b40c8-e233-4cf9-a133-31f12397ad10\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":610426,\"participants\":[{\"id\":996306,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":996307,\"user_id\":18,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"da7b40c8-e233-4cf9-a133-31f12397ad10\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"da7b40c8-e233-4cf9-a133-31f12397ad10\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"da7b40c8-e233-4cf9-a133-31f12397ad10\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"da7b40c8-e233-4cf9-a133-31f12397ad10\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"da7b40c8-e233-4cf9-a133-31f12397ad10\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"da7b40c8-e233-4cf9-a133-31f12397ad10\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"da7b40c8-e233-4cf9-a133-31f12397ad10\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"da7b40c8-e233-4cf9-a133-31f12397ad10\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":610426,\"team_id\":2,\"email\":\"veselin.kulov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"da7b40c8-e233-4cf9-a133-31f12397ad10\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":610426,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"da7b40c8-e233-4cf9-a133-31f12397ad10\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610426} {\"correlation_id\":\"da7b40c8-e233-4cf9-a133-31f12397ad10\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610426,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"da7b40c8-e233-4cf9-a133-31f12397ad10\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":610426,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"da7b40c8-e233-4cf9-a133-31f12397ad10\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":610403,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"4e1aa6ad-3e68-46fe-8129-19feb67cee5f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610403} {\"correlation_id\":\"4e1aa6ad-3e68-46fe-8129-19feb67cee5f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610403,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"4e1aa6ad-3e68-46fe-8129-19feb67cee5f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":610403,\"participants\":[{\"id\":996282,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":996283,\"user_id\":18,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"4e1aa6ad-3e68-46fe-8129-19feb67cee5f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"4e1aa6ad-3e68-46fe-8129-19feb67cee5f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"4e1aa6ad-3e68-46fe-8129-19feb67cee5f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4e1aa6ad-3e68-46fe-8129-19feb67cee5f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"4e1aa6ad-3e68-46fe-8129-19feb67cee5f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"4e1aa6ad-3e68-46fe-8129-19feb67cee5f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"4e1aa6ad-3e68-46fe-8129-19feb67cee5f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"4e1aa6ad-3e68-46fe-8129-19feb67cee5f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":610403,\"team_id\":2,\"email\":\"veselin.kulov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"4e1aa6ad-3e68-46fe-8129-19feb67cee5f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":610403,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"4e1aa6ad-3e68-46fe-8129-19feb67cee5f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610403} {\"correlation_id\":\"4e1aa6ad-3e68-46fe-8129-19feb67cee5f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610403,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"4e1aa6ad-3e68-46fe-8129-19feb67cee5f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":610403,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"4e1aa6ad-3e68-46fe-8129-19feb67cee5f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":610400,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":350,\"stage_id\":34}} {\"correlation_id\":\"c137d466-9e9c-4af1-82c0-9f8639805989\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610400} {\"correlation_id\":\"c137d466-9e9c-4af1-82c0-9f8639805989\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610400,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"c137d466-9e9c-4af1-82c0-9f8639805989\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":610400,\"participants\":[{\"id\":996275,\"user_id\":1460,\"contact_id\":null,\"lead_id\":null},{\"id\":996276,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":996277,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null}]} {\"correlation_id\":\"c137d466-9e9c-4af1-82c0-9f8639805989\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"c137d466-9e9c-4af1-82c0-9f8639805989\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"c137d466-9e9c-4af1-82c0-9f8639805989\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"c137d466-9e9c-4af1-82c0-9f8639805989\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"c137d466-9e9c-4af1-82c0-9f8639805989\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":610400,\"team_id\":2,\"email\":\"aneliya.angelova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"c137d466-9e9c-4af1-82c0-9f8639805989\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":610400,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"c137d466-9e9c-4af1-82c0-9f8639805989\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: ProspectCache - Searching DB for opportunity by owner {\"account_id\":244,\"contact_id\":4487,\"owner_id\":1460} {\"correlation_id\":\"c137d466-9e9c-4af1-82c0-9f8639805989\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: ProspectCache - Fallback DB opportunity search {\"account_id\":244,\"contact_id\":4487} {\"correlation_id\":\"c137d466-9e9c-4af1-82c0-9f8639805989\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: ProspectCache - Opportunity DB search results {\"account_id\":244,\"contact_id\":4487,\"opportunity_id\":350} {\"correlation_id\":\"c137d466-9e9c-4af1-82c0-9f8639805989\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"c137d466-9e9c-4af1-82c0-9f8639805989\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":610400,\"participants_processed\":3,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"c137d466-9e9c-4af1-82c0-9f8639805989\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610400} {\"correlation_id\":\"c137d466-9e9c-4af1-82c0-9f8639805989\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610400,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"c137d466-9e9c-4af1-82c0-9f8639805989\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":610400,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":350,\"stage_id\":34} {\"correlation_id\":\"c137d466-9e9c-4af1-82c0-9f8639805989\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1115,\"provider\":\"google\"} {\"correlation_id\":\"003a1645-a7b4-447f-8217-b1f109d902a1\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:42] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1115,\"provider\":\"google\"} {\"correlation_id\":\"003a1645-a7b4-447f-8217-b1f109d902a1\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:42] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"003a1645-a7b4-447f-8217-b1f109d902a1\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:42] local.INFO: [Calendar] Processing sync {\"calendarId\":\"2676cb6d-f86c-427e-bf78-591e388e3c1e\",\"from\":null,\"to\":null,\"delta\":\"CJ_x49O3jpIDEJ_x49O3jpIDGAUgw67KlwMow67KlwM=\",\"last_sync\":\"2026-01-19 07:48:40\",\"dateMode\":\"daily\"} {\"correlation_id\":\"003a1645-a7b4-447f-8217-b1f109d902a1\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:42] local.WARNING: [Pipedrive] Account not connected for user {\"userId\":\"e6538737-e7b4-455f-a37a-3e79b665a220\",\"account\":{\"Jiminny\\\\Models\\\\SocialAccount\":{\"id\":1116,\"sociable_id\":241,\"provider_user_id\":\"19555731\",\"expires\":1775683749,\"refresh_token_expires\":null,\"provider\":\"pipedrive\",\"state\":\"full-refresh\",\"auth_scope\":\"base,deals:full,activities:full,contacts:full,search:read\",\"retry_after\":null,\"created_at\":\"2023-09-08 09:44:29\",\"updated_at\":\"2026-04-08 22:58:34\"}}} {\"correlation_id\":\"003a1645-a7b4-447f-8217-b1f109d902a1\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:42] local.INFO: [CrmOwnerResolver] Integration owner is not connected, attempting team members {\"crm_provider\":\"pipedrive\",\"crm_owner\":241,\"team_id\":19} {\"correlation_id\":\"003a1645-a7b4-447f-8217-b1f109d902a1\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:42] local.INFO: [CrmOwnerResolver] No team members found with active crm connection {\"crm_provider\":\"pipedrive\",\"team_id\":19} {\"correlation_id\":\"003a1645-a7b4-447f-8217-b1f109d902a1\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:42] local.INFO: [CrmOwnerResolver] No team member found with active crm connection {\"crm_provider\":\"pipedrive\",\"team_id\":19} {\"correlation_id\":\"003a1645-a7b4-447f-8217-b1f109d902a1\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:42] local.WARNING: [Calendar] CRM disconnected for user so events will not be matched {\"provider\":\"pipedrive\",\"user_id\":241,\"message\":\"Your Pipedrive account has become disconnected. Please login to Jiminny to reconnect.\"} {\"correlation_id\":\"003a1645-a7b4-447f-8217-b1f109d902a1\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:42] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1115,\"provider\":\"google\"} {\"correlation_id\":\"003a1645-a7b4-447f-8217-b1f109d902a1\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:42] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1115,\"provider\":\"google\"} {\"correlation_id\":\"003a1645-a7b4-447f-8217-b1f109d902a1\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:42] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"003a1645-a7b4-447f-8217-b1f109d902a1\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:42] local.INFO: [Google Calendar] Failed to watch channel for calendar {\"calendarId\":\"2676cb6d-f86c-427e-bf78-591e388e3c1e\",\"code\":400,\"reason\":\"{\n \\\"error\\\": {\n \\\"errors\\\": [\n {\n \\\"domain\\\": \\\"global\\\",\n \\\"reason\\\": \\\"push.webhookUrlNotHttps\\\",\n \\\"message\\\": \\\"WebHook callback must be HTTPS: /webhook/calendar/google?resourceType=event\\\"\n }\n ],\n \\\"code\\\": 400,\n \\\"message\\\": \\\"WebHook callback must be HTTPS: /webhook/calendar/google?resourceType=event\\\"\n }\n}\"} {\"correlation_id\":\"003a1645-a7b4-447f-8217-b1f109d902a1\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:42] local.WARNING: [Calendar] Sync failed {\"calendarId\":\"2676cb6d-f86c-427e-bf78-591e388e3c1e\",\"code\":400,\"reason\":\"{\n \\\"error\\\": {\n \\\"errors\\\": [\n {\n \\\"domain\\\": \\\"global\\\",\n \\\"reason\\\": \\\"push.webhookUrlNotHttps\\\",\n \\\"message\\\": \\\"WebHook callback must be HTTPS: /webhook/calendar/google?resourceType=event\\\"\n }\n ],\n \\\"code\\\": 400,\n \\\"message\\\": \\\"WebHook callback must be HTTPS: /webhook/calendar/google?resourceType=event\\\"\n }\n}\"} {\"correlation_id\":\"003a1645-a7b4-447f-8217-b1f109d902a1\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:42] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1421,\"provider\":\"office\"} {\"correlation_id\":\"f66a7935-f1a6-4d93-b53d-1464e6e79019\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:42] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1421,\"provider\":\"office\"} {\"correlation_id\":\"f66a7935-f1a6-4d93-b53d-1464e6e79019\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:42] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"f66a7935-f1a6-4d93-b53d-1464e6e79019\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:42] local.INFO: [Calendar] Processing sync {\"calendarId\":\"9e8b1a2c-1a8f-42bd-b161-810fc0baf540\",\"from\":null,\"to\":null,\"delta\":\"R0usmcdvmMuZCBYV0hguCLlvcOB4kXlhlC7KgH1SnZwTrZ3faZv1fXPQqJhxe_L9AxWWlb-wASsjGiiWlhsBUg9MFb3ZdlAYerVV_ZirRPbsKWCxEXhybD90arJmok_M4ecGFUQ9_BIGu-c6RAnJy2TRKZ7gPTsJi_8TGceGAuqimlhm4G4mjDLvYVVwImjjU7M3xJvUzL47dLOGNTJCww.k1TST0VEYCgbFOkwa3ysYMi100FtVfkzfqlXLnV6gPg\",\"last_sync\":\"2026-05-11 06:13:36\",\"dateMode\":\"daily\"} {\"correlation_id\":\"f66a7935-f1a6-4d93-b53d-1464e6e79019\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:42] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"f66a7935-f1a6-4d93-b53d-1464e6e79019\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:42] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"f66a7935-f1a6-4d93-b53d-1464e6e79019\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:42] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"f66a7935-f1a6-4d93-b53d-1464e6e79019\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:42] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":89,\"team_id\":2} {\"correlation_id\":\"f66a7935-f1a6-4d93-b53d-1464e6e79019\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:42] local.INFO: [MS Office Calendar] Skipping delta sync for daily mode {\"calendarId\":\"9e8b1a2c-1a8f-42bd-b161-810fc0baf540\"} {\"correlation_id\":\"f66a7935-f1a6-4d93-b53d-1464e6e79019\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}","depth":4,"bounds":{"left":0.62200797,"top":0.10614525,"width":0.37799203,"height":0.89385474},"on_screen":true,"value":"[2026-05-11 10:41:16] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"ee9b9096-b1f1-4e2b-ac17-af0003eb00c0\",\"trace_id\":\"8904c61b-d5b4-44cc-91bf-68f0f73222c0\"}\n[2026-05-11 10:41:16] local.INFO: [EmailSchedule] STARTING batch process {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"ee9b9096-b1f1-4e2b-ac17-af0003eb00c0\",\"trace_id\":\"8904c61b-d5b4-44cc-91bf-68f0f73222c0\"}\n[2026-05-11 10:41:17] local.INFO: [EmailSchedule] FINISHED batch process {\"host\":\"docker_lamp_1\",\"processed\":0} {\"correlation_id\":\"ee9b9096-b1f1-4e2b-ac17-af0003eb00c0\",\"trace_id\":\"8904c61b-d5b4-44cc-91bf-68f0f73222c0\"}\n[2026-05-11 10:41:17] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":60.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"ee9b9096-b1f1-4e2b-ac17-af0003eb00c0\",\"trace_id\":\"8904c61b-d5b4-44cc-91bf-68f0f73222c0\"}\n[2026-05-11 10:41:21] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"crm:sync-hubspot-objects\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"13c8d2de-a598-41db-b4ca-9799a4bee946\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:21] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"crm:sync-hubspot-objects\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":60.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"13c8d2de-a598-41db-b4ca-9799a4bee946\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:21] local.INFO: [SyncHubspotObjects] Starting sync {\"team\":\"b2b115eb-93ce-4d1b-929c-173757df8fba\",\"usage\":24894088,\"real_usage\":65011712,\"pid\":62058} {\"correlation_id\":\"81433689-3fcd-4128-8b21-068ce5ee75be\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:21] local.WARNING: [HubSpot] Account not connected for user {\"userId\":\"33e34a7a-1c02-4f04-87ac-22c3a385e6e3\",\"account\":{\"Jiminny\\\\Models\\\\SocialAccount\":{\"id\":306,\"sociable_id\":109,\"provider_user_id\":\"11348452\",\"expires\":1701077403,\"refresh_token_expires\":null,\"provider\":\"hubspot\",\"state\":\"full-refresh\",\"auth_scope\":null,\"retry_after\":null,\"created_at\":\"2020-09-01 16:59:04\",\"updated_at\":\"2023-11-27 09:30:03\"}}} {\"correlation_id\":\"81433689-3fcd-4128-8b21-068ce5ee75be\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:21] local.INFO: [CrmOwnerResolver] Integration owner is not connected, attempting team members {\"crm_provider\":\"hubspot\",\"crm_owner\":109,\"team_id\":29} {\"correlation_id\":\"81433689-3fcd-4128-8b21-068ce5ee75be\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:21] local.INFO: [CrmOwnerResolver] No team members found with active crm connection {\"crm_provider\":\"hubspot\",\"team_id\":29} {\"correlation_id\":\"81433689-3fcd-4128-8b21-068ce5ee75be\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:21] local.INFO: [CrmOwnerResolver] No team member found with active crm connection {\"crm_provider\":\"hubspot\",\"team_id\":29} {\"correlation_id\":\"81433689-3fcd-4128-8b21-068ce5ee75be\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:21] local.INFO: [SyncHubspotObjects] Sync finished {\"team\":\"b2b115eb-93ce-4d1b-929c-173757df8fba\",\"provider\":\"hubspot\",\"status\":\"disconnected\",\"duration_ms\":35.85,\"usage\":24956128,\"real_usage\":65011712,\"pid\":62058,\"reason\":\"Your HubSpot account has become disconnected. Please login to Jiminny to reconnect.\"} {\"correlation_id\":\"81433689-3fcd-4128-8b21-068ce5ee75be\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:21] local.INFO: [SyncHubspotObjects] Starting sync {\"team\":\"abae74b8-bfa8-4383-9a7f-89f4bf2bdbb4\",\"usage\":24914344,\"real_usage\":65011712,\"pid\":62058} {\"correlation_id\":\"d2cb4aff-14c0-474f-89f2-f5c69dafe934\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:21] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"d2cb4aff-14c0-474f-89f2-f5c69dafe934\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:21] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"d2cb4aff-14c0-474f-89f2-f5c69dafe934\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:21] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d2cb4aff-14c0-474f-89f2-f5c69dafe934\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:21] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"d2cb4aff-14c0-474f-89f2-f5c69dafe934\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:21] local.INFO: [HubSpot] Syncing opportunities using strategy: lastModified {\"team\":2} {\"correlation_id\":\"d2cb4aff-14c0-474f-89f2-f5c69dafe934\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:21] local.INFO: [Hubspot] Pagination completed {\"team_id\":2,\"endpoint\":\"https://api.hubapi.com/crm/v3/objects/deals/search\",\"total_requests\":1,\"total_records_fetched\":0,\"total_elapsed_seconds\":0.26,\"average_seconds_per_request\":0.26} {\"correlation_id\":\"d2cb4aff-14c0-474f-89f2-f5c69dafe934\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:21] local.INFO: [HubSpot] Synced opportunities {\"team\":2,\"strategies\":\"lastModified\",\"sync_count\":0,\"total\":0,\"last_synced_id\":null,\"duration_ms\":288.63} {\"correlation_id\":\"d2cb4aff-14c0-474f-89f2-f5c69dafe934\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:21] local.INFO: [SyncHubspotObjects] Sync finished {\"team\":\"abae74b8-bfa8-4383-9a7f-89f4bf2bdbb4\",\"provider\":\"hubspot\",\"status\":\"completed\",\"duration_ms\":324.99,\"usage\":24978696,\"real_usage\":65011712,\"pid\":62058} {\"correlation_id\":\"d2cb4aff-14c0-474f-89f2-f5c69dafe934\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:22] local.INFO: [SyncHubspotObjects] Starting sync {\"team\":\"c6b9d6b0-b48d-4832-a68c-a57d60651888\",\"usage\":24956624,\"real_usage\":65011712,\"pid\":62058} {\"correlation_id\":\"955d9400-2f7f-4170-be31-43bafa1ef3bd\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:22] local.WARNING: [HubSpot] Account not connected for user {\"userId\":\"71e3aac5-fb66-47c5-a236-2d051ae3e319\",\"account\":null} {\"correlation_id\":\"955d9400-2f7f-4170-be31-43bafa1ef3bd\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:22] local.INFO: [CrmOwnerResolver] Integration owner is not connected, attempting team members {\"crm_provider\":\"hubspot\",\"crm_owner\":256,\"team_id\":49} {\"correlation_id\":\"955d9400-2f7f-4170-be31-43bafa1ef3bd\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:22] local.INFO: [CrmOwnerResolver] No team members found with active crm connection {\"crm_provider\":\"hubspot\",\"team_id\":49} {\"correlation_id\":\"955d9400-2f7f-4170-be31-43bafa1ef3bd\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:22] local.INFO: [CrmOwnerResolver] No team member found with active crm connection {\"crm_provider\":\"hubspot\",\"team_id\":49} {\"correlation_id\":\"955d9400-2f7f-4170-be31-43bafa1ef3bd\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:22] local.INFO: [SyncHubspotObjects] Sync finished {\"team\":\"c6b9d6b0-b48d-4832-a68c-a57d60651888\",\"provider\":\"hubspot\",\"status\":\"disconnected\",\"duration_ms\":17.51,\"usage\":24941048,\"real_usage\":65011712,\"pid\":62058,\"reason\":\"Social account for HubSpot cannot be found. Please login to Jiminny to connect.\"} {\"correlation_id\":\"955d9400-2f7f-4170-be31-43bafa1ef3bd\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:22] local.INFO: [SyncHubspotObjects] Starting sync {\"team\":\"b2d49a54-b645-4637-a7ae-a86cfce6e8e4\",\"usage\":24901688,\"real_usage\":65011712,\"pid\":62058} {\"correlation_id\":\"6d0b5717-9160-4c2f-ad12-56e42a2c544a\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:22] local.WARNING: [HubSpot] Account not connected for user {\"userId\":\"2ac0447f-3c8c-4ce0-baeb-b63ddb76fa9b\",\"account\":null} {\"correlation_id\":\"6d0b5717-9160-4c2f-ad12-56e42a2c544a\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:22] local.INFO: [CrmOwnerResolver] Integration owner is not connected, attempting team members {\"crm_provider\":\"hubspot\",\"crm_owner\":130,\"team_id\":42} {\"correlation_id\":\"6d0b5717-9160-4c2f-ad12-56e42a2c544a\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:22] local.INFO: [CrmOwnerResolver] No team members found with active crm connection {\"crm_provider\":\"hubspot\",\"team_id\":42} {\"correlation_id\":\"6d0b5717-9160-4c2f-ad12-56e42a2c544a\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:22] local.INFO: [CrmOwnerResolver] No team member found with active crm connection {\"crm_provider\":\"hubspot\",\"team_id\":42} {\"correlation_id\":\"6d0b5717-9160-4c2f-ad12-56e42a2c544a\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:22] local.INFO: [SyncHubspotObjects] Sync finished {\"team\":\"b2d49a54-b645-4637-a7ae-a86cfce6e8e4\",\"provider\":\"hubspot\",\"status\":\"disconnected\",\"duration_ms\":10.99,\"usage\":24944632,\"real_usage\":65011712,\"pid\":62058,\"reason\":\"Social account for HubSpot cannot be found. Please login to Jiminny to connect.\"} {\"correlation_id\":\"6d0b5717-9160-4c2f-ad12-56e42a2c544a\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:39] local.INFO: [HubSpot Journal Polling] No data {\"correlation_id\":\"1f8f11e6-0cf5-49dd-b8d7-56f51ec27f8e\",\"trace_id\":\"0f08076e-448d-487e-99a0-e73dae5c0ad4\"}\n[2026-05-11 10:41:39] local.WARNING: [HubSpot Journal Polling] Maximum empty results reached, stopping {\"empty_results\":5,\"max_empty_results\":5} {\"correlation_id\":\"1f8f11e6-0cf5-49dd-b8d7-56f51ec27f8e\",\"trace_id\":\"0f08076e-448d-487e-99a0-e73dae5c0ad4\"}\n[2026-05-11 10:41:39] local.WARNING: [HubSpot Journal Polling] Maximum empty results reached, stopping {\"empty_results\":5,\"max_empty_results\":5} {\"correlation_id\":\"1f8f11e6-0cf5-49dd-b8d7-56f51ec27f8e\",\"trace_id\":\"0f08076e-448d-487e-99a0-e73dae5c0ad4\"}\n[2026-05-11 10:41:39] local.INFO: [HubSpot Journal Polling] Service ending {\"runtime_seconds\":57,\"total_cycles\":5,\"files_downloaded\":0,\"empty_files\":0,\"other_portal_skipped\":0,\"total_events\":0,\"events_per_file\":0,\"avg_api_ms\":296.8,\"avg_download_ms\":0.0,\"avg_transform_ms\":0.0,\"avg_process_ms\":0.0,\"peak_memory_mb\":99.73} {\"correlation_id\":\"1f8f11e6-0cf5-49dd-b8d7-56f51ec27f8e\",\"trace_id\":\"0f08076e-448d-487e-99a0-e73dae5c0ad4\"}\n[2026-05-11 10:41:39] local.INFO: [HubSpot Journal Polling] Saved offset to database on cleanup {\"offset\":\"019e15a9-9ea0-7da7-87bc-82592e3ccf0d\"} {\"correlation_id\":\"1f8f11e6-0cf5-49dd-b8d7-56f51ec27f8e\",\"trace_id\":\"0f08076e-448d-487e-99a0-e73dae5c0ad4\"}\n[2026-05-11 10:41:39] local.INFO: [HubSpot Journal Polling] Released polling lock {\"correlation_id\":\"1f8f11e6-0cf5-49dd-b8d7-56f51ec27f8e\",\"trace_id\":\"0f08076e-448d-487e-99a0-e73dae5c0ad4\"}\n[2026-05-11 10:42:07] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"1aa66921-0a71-4d61-8cd7-0663242dca44\",\"trace_id\":\"f6b1d739-39b6-4043-a81e-a1af2c76d401\"}\n[2026-05-11 10:42:07] local.INFO: [ScheduleBotCommand] Number of activities to be captured: 0 {\"correlation_id\":\"1aa66921-0a71-4d61-8cd7-0663242dca44\",\"trace_id\":\"f6b1d739-39b6-4043-a81e-a1af2c76d401\"}\n[2026-05-11 10:42:07] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":60.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"1aa66921-0a71-4d61-8cd7-0663242dca44\",\"trace_id\":\"f6b1d739-39b6-4043-a81e-a1af2c76d401\"}\n[2026-05-11 10:42:10] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"cc420ba5-39bf-4183-95ae-48962a9c124b\",\"trace_id\":\"8daf6081-5c8a-4fae-98d8-e317d8580bc3\"}\n[2026-05-11 10:42:10] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":60.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"cc420ba5-39bf-4183-95ae-48962a9c124b\",\"trace_id\":\"8daf6081-5c8a-4fae-98d8-e317d8580bc3\"}\n[2026-05-11 10:42:13] local.NOTICE: Monitoring start {\"correlation_id\":\"50696062-587c-4c43-8075-7229916ea46f\",\"trace_id\":\"b58f754c-906f-413b-80e6-f89189dbe078\"}\n[2026-05-11 10:42:13] local.NOTICE: Monitoring end {\"correlation_id\":\"50696062-587c-4c43-8075-7229916ea46f\",\"trace_id\":\"b58f754c-906f-413b-80e6-f89189dbe078\"}\n[2026-05-11 10:42:17] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"f70656cd-6de3-4f8b-bb9d-74a941c552c4\",\"trace_id\":\"c780d8e3-2abd-41a3-a9dc-7c2be6aa22d4\"}\n[2026-05-11 10:42:17] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":60.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"f70656cd-6de3-4f8b-bb9d-74a941c552c4\",\"trace_id\":\"c780d8e3-2abd-41a3-a9dc-7c2be6aa22d4\"}\n[2026-05-11 10:42:20] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"b2e73601-1777-4ddb-a152-fe85f4c89afc\",\"trace_id\":\"8bac5054-216a-4605-b13b-9efab8ab5341\"}\n[2026-05-11 10:42:20] local.INFO: [EmailSchedule] STARTING batch process {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"b2e73601-1777-4ddb-a152-fe85f4c89afc\",\"trace_id\":\"8bac5054-216a-4605-b13b-9efab8ab5341\"}\n[2026-05-11 10:42:20] local.INFO: [EmailSchedule] FINISHED batch process {\"host\":\"docker_lamp_1\",\"processed\":0} {\"correlation_id\":\"b2e73601-1777-4ddb-a152-fe85f4c89afc\",\"trace_id\":\"8bac5054-216a-4605-b13b-9efab8ab5341\"}\n[2026-05-11 10:42:20] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":60.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"b2e73601-1777-4ddb-a152-fe85f4c89afc\",\"trace_id\":\"8bac5054-216a-4605-b13b-9efab8ab5341\"}\n[2026-05-11 10:42:31] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"conference:monitor:count\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"175d1973-e4ab-47f7-bb25-14bc3f0a1cce\",\"trace_id\":\"723f475a-f2fc-4235-8163-c2a8ed5777e6\"}\n[2026-05-11 10:42:31] local.INFO: Running conference:monitor:count command for activities in (2026-05-11 10:40:00, 2026-05-11 10:42:00] {\"correlation_id\":\"175d1973-e4ab-47f7-bb25-14bc3f0a1cce\",\"trace_id\":\"723f475a-f2fc-4235-8163-c2a8ed5777e6\"}\n[2026-05-11 10:42:31] local.INFO: [conference:monitor:count] No activities found in (2026-05-11 10:40:00, 2026-05-11 10:42:00] {\"correlation_id\":\"175d1973-e4ab-47f7-bb25-14bc3f0a1cce\",\"trace_id\":\"723f475a-f2fc-4235-8163-c2a8ed5777e6\"}\n[2026-05-11 10:42:31] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"conference:monitor:count\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":60.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"175d1973-e4ab-47f7-bb25-14bc3f0a1cce\",\"trace_id\":\"723f475a-f2fc-4235-8163-c2a8ed5777e6\"}\n[2026-05-11 10:42:36] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:create\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"4243f43b-9d70-4dd0-9c98-3122eb9445ed\",\"trace_id\":\"0c811e1d-ed7e-4aa7-ba81-a14a0cf4b306\"}\n[2026-05-11 10:42:36] local.INFO: [EmailSchedule] STARTING batch create {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"4243f43b-9d70-4dd0-9c98-3122eb9445ed\",\"trace_id\":\"0c811e1d-ed7e-4aa7-ba81-a14a0cf4b306\"}\n[2026-05-11 10:42:36] local.INFO: [EmailSchedule] FINISHED batch create {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"4243f43b-9d70-4dd0-9c98-3122eb9445ed\",\"trace_id\":\"0c811e1d-ed7e-4aa7-ba81-a14a0cf4b306\"}\n[2026-05-11 10:42:36] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:create\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":60.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"4243f43b-9d70-4dd0-9c98-3122eb9445ed\",\"trace_id\":\"0c811e1d-ed7e-4aa7-ba81-a14a0cf4b306\"}\n[2026-05-11 10:42:39] local.INFO: [Jiminny\\Jobs\\Mailbox\\CreateBatches] processed 2 inboxes and created 0 batches {\"userId\":null,\"batchSize\":30,\"maxBatches\":1000} {\"correlation_id\":\"2328b74c-d9ea-43ef-95db-20ea7c6ec9f5\",\"trace_id\":\"0c811e1d-ed7e-4aa7-ba81-a14a0cf4b306\"}\n[2026-05-11 10:42:54] local.INFO: [HubSpot Webhook] Signature validation started {\"method\":\"POST\",\"uri\":\"/webhook/conference/hubspot/events\",\"has_v3_signature\":true,\"has_v1_signature\":true} {\"correlation_id\":\"35391537-e747-4658-94f7-9d3d8c65eab6\",\"trace_id\":\"163e44b3-9f8e-4957-b04d-4937c33d30e6\"}\n[2026-05-11 10:42:54] local.INFO: [HubSpot Webhook] Event received {\"payload\":[{\"eventId\":1531490889,\"subscriptionId\":5683273,\"portalId\":4392066,\"appId\":38482,\"occurredAt\":1778478181931,\"subscriptionType\":\"company.associationChange\",\"attemptNumber\":8,\"changeSource\":\"USER\",\"associationType\":\"COMPANY_TO_DEAL\",\"fromObjectId\":13266161465,\"toObjectId\":60142058500,\"associationRemoved\":false,\"isPrimaryAssociation\":false}],\"event_count\":1} {\"correlation_id\":\"35391537-e747-4658-94f7-9d3d8c65eab6\",\"trace_id\":\"163e44b3-9f8e-4957-b04d-4937c33d30e6\"}\n[2026-05-11 10:42:54] local.INFO: [HubSpot Webhook Job] Processing webhook events {\"event_count\":1,\"local_count\":1,\"forward_count\":1} {\"correlation_id\":\"bb5ee15c-0826-40ef-a4c4-83761c307c7b\",\"trace_id\":\"163e44b3-9f8e-4957-b04d-4937c33d30e6\"}\n[2026-05-11 10:42:55] local.INFO: [Webhook Forwarder] Forwarding to instance {\"url\":\"https://uranus.staging.jiminny.com/internal/webhook-receiver/hubspot\",\"route\":\"internal.webhook-receiver.hubspot\",\"event_count\":1} {\"correlation_id\":\"bb5ee15c-0826-40ef-a4c4-83761c307c7b\",\"trace_id\":\"163e44b3-9f8e-4957-b04d-4937c33d30e6\"}\n[2026-05-11 10:42:55] local.ERROR: [Webhook Forwarder] Failed to forward to instance {\"url\":\"https://uranus.staging.jiminny.com\",\"route\":\"internal.webhook-receiver.hubspot\",\"error\":\"Server error: `POST https://uranus.staging.jiminny.com/internal/webhook-receiver/hubspot` resulted in a `503 Service Temporarily Unavailable` response:\n<html>\n\n<head><title>503 Service Temporarily Unavailable</title></head>\n\n<body>\n\n<center><h1>503 Service Temporarily Una (truncated...)\n\",\"code\":503} {\"correlation_id\":\"bb5ee15c-0826-40ef-a4c4-83761c307c7b\",\"trace_id\":\"163e44b3-9f8e-4957-b04d-4937c33d30e6\"}\n[2026-05-11 10:42:55] local.INFO: [Webhook Forwarder] Forwarding to instance {\"url\":\"https://app.qai.jiminny.com/internal/webhook-receiver/hubspot\",\"route\":\"internal.webhook-receiver.hubspot\",\"event_count\":1} {\"correlation_id\":\"bb5ee15c-0826-40ef-a4c4-83761c307c7b\",\"trace_id\":\"163e44b3-9f8e-4957-b04d-4937c33d30e6\"}\n[2026-05-11 10:42:56] local.INFO: [Webhook Forwarder] Successfully forwarded to instance {\"url\":\"https://app.qai.jiminny.com/internal/webhook-receiver/hubspot\",\"status_code\":202,\"response\":{\"status\":\"accepted\",\"message\":\"Events queued for processing\",\"event_count\":1}} {\"correlation_id\":\"bb5ee15c-0826-40ef-a4c4-83761c307c7b\",\"trace_id\":\"163e44b3-9f8e-4957-b04d-4937c33d30e6\"}\n[2026-05-11 10:42:56] local.INFO: [BatchSyncCollector] Stored webhook in Redis {\"object_type\":\"company\",\"crm_provider_id\":\"13266161465\",\"event_type\":\"association_change\",\"configuration_id\":2,\"batch_key\":\"batch_sync_company:ids:2:all\",\"was_new\":true,\"current_size\":1} {\"correlation_id\":\"bb5ee15c-0826-40ef-a4c4-83761c307c7b\",\"trace_id\":\"163e44b3-9f8e-4957-b04d-4937c33d30e6\"}\n[2026-05-11 10:43:14] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"a2a4326d-0416-4ea7-8eac-370e0fee93fb\",\"trace_id\":\"49cf2405-4c7b-48ce-9aac-074f08037e94\"}\n[2026-05-11 10:43:14] local.INFO: [ScheduleBotCommand] Number of activities to be captured: 0 {\"correlation_id\":\"a2a4326d-0416-4ea7-8eac-370e0fee93fb\",\"trace_id\":\"49cf2405-4c7b-48ce-9aac-074f08037e94\"}\n[2026-05-11 10:43:14] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":60.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"a2a4326d-0416-4ea7-8eac-370e0fee93fb\",\"trace_id\":\"49cf2405-4c7b-48ce-9aac-074f08037e94\"}\n[2026-05-11 10:43:18] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":615092,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"e7be542b-ee51-44b8-a563-a2eac45b3413\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:18] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":615092} {\"correlation_id\":\"e7be542b-ee51-44b8-a563-a2eac45b3413\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:18] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":615092,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"e7be542b-ee51-44b8-a563-a2eac45b3413\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:18] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":615092,\"participants\":[{\"id\":1004102,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":1004103,\"user_id\":89,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"e7be542b-ee51-44b8-a563-a2eac45b3413\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:19] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"e7be542b-ee51-44b8-a563-a2eac45b3413\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:19] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"e7be542b-ee51-44b8-a563-a2eac45b3413\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:19] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"e7be542b-ee51-44b8-a563-a2eac45b3413\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:19] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"e7be542b-ee51-44b8-a563-a2eac45b3413\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:20] local.INFO: [Prospect match] Cache miss, calling the API {\"identifier_type\":\"email\",\"identifier\":\"nikolay.nikolov@jiminny.com\"} {\"correlation_id\":\"e7be542b-ee51-44b8-a563-a2eac45b3413\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:20] local.INFO: [Hubspot] Failed to fetch contact {\"email\":\"nikolay.nikolov@jiminny.com\",\"reason\":\"[404] Client error: `GET https://api.hubapi.com/crm/v3/objects/contacts/nikolay.nikolov%40jiminny.com?properties=email%2Cfirstname%2Clastname%2Ccountry%2Cphone%2Cmobilephone%2Cjobtitle%2Chubspot_owner_id%2Cassociatedcompanyid%2Cphoto&archived=0&idProperty=email` resulted in a `404 Not Found` response\"} {\"correlation_id\":\"e7be542b-ee51-44b8-a563-a2eac45b3413\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:20] local.INFO: [Prospect match] API returned empty result, caching the miss with empty prospect data {\"identifier_type\":\"email\",\"identifier\":\"nikolay.nikolov@jiminny.com\"} {\"correlation_id\":\"e7be542b-ee51-44b8-a563-a2eac45b3413\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:21] local.INFO: [Hubspot] Pagination completed {\"team_id\":2,\"endpoint\":\"https://api.hubapi.com/crm/v3/objects/contact/search\",\"total_requests\":1,\"total_records_fetched\":0,\"total_elapsed_seconds\":0.38,\"average_seconds_per_request\":0.38} {\"correlation_id\":\"e7be542b-ee51-44b8-a563-a2eac45b3413\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:22] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"ab7124d8-7931-41ef-8341-93a3305f65eb\",\"trace_id\":\"4564d24d-bf9e-486b-ac8e-0f69c9b286d8\"}\n[2026-05-11 10:43:22] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":60.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"ab7124d8-7931-41ef-8341-93a3305f65eb\",\"trace_id\":\"4564d24d-bf9e-486b-ac8e-0f69c9b286d8\"}\n[2026-05-11 10:43:22] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"nikolay.nikolov@jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"e7be542b-ee51-44b8-a563-a2eac45b3413\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:22] local.INFO: [Prospect match] Cache miss {\"identifier_type\":\"domain\",\"identifier\":\"jiminny.com\",\"crm\":\"hubspot\"} {\"correlation_id\":\"e7be542b-ee51-44b8-a563-a2eac45b3413\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:22] local.INFO: [Prospect match] Cache miss, calling the API {\"identifier_type\":\"domain\",\"identifier\":\"jiminny.com\"} {\"correlation_id\":\"e7be542b-ee51-44b8-a563-a2eac45b3413\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:23] local.INFO: [HubSpot] importAccount {\"crm_provider_id\":\"749766179\",\"config_id\":2} {\"correlation_id\":\"e7be542b-ee51-44b8-a563-a2eac45b3413\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:23] local.INFO: [HubSpot] CRM Search requested {\"request\":{\"filterGroups\":[{\"filters\":[{\"propertyName\":\"associations.company\",\"operator\":\"EQ\",\"value\":\"749766179\"},{\"propertyName\":\"dealstage\",\"operator\":\"NOT_IN\",\"values\":[\"closedwon\",\"4040964\",\"59247967\"]},{\"propertyName\":\"dealstage\",\"operator\":\"NOT_IN\",\"values\":[\"closedlost\",\"4040965\",\"59247968\"]}]}],\"sorts\":[{\"propertyName\":\"modifieddate\",\"direction\":\"DESCENDING\"}],\"properties\":[\"dealname\",\"amount\",\"hubspot_owner_id\",\"pipeline\",\"dealstage\",\"closedate\",\"deal_currency_code\"],\"limit\":200}} {\"correlation_id\":\"e7be542b-ee51-44b8-a563-a2eac45b3413\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:23] local.INFO: [Hubspot] Pagination completed {\"team_id\":2,\"endpoint\":\"https://api.hubapi.com/crm/v3/objects/deals/search\",\"total_requests\":1,\"total_records_fetched\":10,\"total_elapsed_seconds\":0.3,\"average_seconds_per_request\":0.3} {\"correlation_id\":\"e7be542b-ee51-44b8-a563-a2eac45b3413\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:24] local.INFO: [ EsUpdateProcessManager ] Finished updating entities in ES {\"worker\":\"\",\"peak_memory\":\"99.73 MB\",\"elapsed_seconds\":3.22,\"update_target\":\"activities\",\"should_iterate_again\":false} {\"correlation_id\":\"4ad05333-9afb-492e-9f0f-b2909ac45b32\",\"trace_id\":\"3d8feb24-b173-4158-b0a4-4cf33af85066\"}\n[2026-05-11 10:43:24] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":615092,\"team_id\":2,\"email\":\"nikolay.nikolov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"e7be542b-ee51-44b8-a563-a2eac45b3413\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:24] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":615092,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"e7be542b-ee51-44b8-a563-a2eac45b3413\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:24] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":615092} {\"correlation_id\":\"e7be542b-ee51-44b8-a563-a2eac45b3413\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:24] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":615092,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"e7be542b-ee51-44b8-a563-a2eac45b3413\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:24] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":615092,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"e7be542b-ee51-44b8-a563-a2eac45b3413\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:24] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":614436,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"a34067d1-038f-48bf-af25-c9e09374ce62\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:24] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":614436} {\"correlation_id\":\"a34067d1-038f-48bf-af25-c9e09374ce62\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:24] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":614436,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"a34067d1-038f-48bf-af25-c9e09374ce62\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:24] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":614436,\"participants\":[{\"id\":1002751,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":1002752,\"user_id\":89,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"a34067d1-038f-48bf-af25-c9e09374ce62\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:24] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"a34067d1-038f-48bf-af25-c9e09374ce62\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:24] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"a34067d1-038f-48bf-af25-c9e09374ce62\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:24] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"a34067d1-038f-48bf-af25-c9e09374ce62\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:24] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"a34067d1-038f-48bf-af25-c9e09374ce62\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:24] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"nikolay.nikolov@jiminny.com\"} {\"correlation_id\":\"a34067d1-038f-48bf-af25-c9e09374ce62\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:24] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"nikolay.nikolov@jiminny.com\"} {\"correlation_id\":\"a34067d1-038f-48bf-af25-c9e09374ce62\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:24] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"nikolay.nikolov@jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"a34067d1-038f-48bf-af25-c9e09374ce62\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:24] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":614436,\"team_id\":2,\"email\":\"nikolay.nikolov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"a34067d1-038f-48bf-af25-c9e09374ce62\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:24] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":614436,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"a34067d1-038f-48bf-af25-c9e09374ce62\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:24] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":614436} {\"correlation_id\":\"a34067d1-038f-48bf-af25-c9e09374ce62\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:24] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":614436,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"a34067d1-038f-48bf-af25-c9e09374ce62\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:24] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":614436,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"a34067d1-038f-48bf-af25-c9e09374ce62\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":614382,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"ee88af0b-20d3-4911-a378-bea7fc6dc3f6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":614382} {\"correlation_id\":\"ee88af0b-20d3-4911-a378-bea7fc6dc3f6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":614382,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"ee88af0b-20d3-4911-a378-bea7fc6dc3f6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":614382,\"participants\":[{\"id\":1002632,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":1002633,\"user_id\":89,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"ee88af0b-20d3-4911-a378-bea7fc6dc3f6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"ee88af0b-20d3-4911-a378-bea7fc6dc3f6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"ee88af0b-20d3-4911-a378-bea7fc6dc3f6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"ee88af0b-20d3-4911-a378-bea7fc6dc3f6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"ee88af0b-20d3-4911-a378-bea7fc6dc3f6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"nikolay.nikolov@jiminny.com\"} {\"correlation_id\":\"ee88af0b-20d3-4911-a378-bea7fc6dc3f6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"nikolay.nikolov@jiminny.com\"} {\"correlation_id\":\"ee88af0b-20d3-4911-a378-bea7fc6dc3f6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"nikolay.nikolov@jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"ee88af0b-20d3-4911-a378-bea7fc6dc3f6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":614382,\"team_id\":2,\"email\":\"nikolay.nikolov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"ee88af0b-20d3-4911-a378-bea7fc6dc3f6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":614382,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"ee88af0b-20d3-4911-a378-bea7fc6dc3f6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":614382} {\"correlation_id\":\"ee88af0b-20d3-4911-a378-bea7fc6dc3f6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":614382,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"ee88af0b-20d3-4911-a378-bea7fc6dc3f6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":614382,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"ee88af0b-20d3-4911-a378-bea7fc6dc3f6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":614381,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"535b99f0-d38e-4b9c-bf19-3751ce4e6a1d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":614381} {\"correlation_id\":\"535b99f0-d38e-4b9c-bf19-3751ce4e6a1d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":614381,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"535b99f0-d38e-4b9c-bf19-3751ce4e6a1d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":614381,\"participants\":[{\"id\":1002630,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":1002631,\"user_id\":89,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"535b99f0-d38e-4b9c-bf19-3751ce4e6a1d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"535b99f0-d38e-4b9c-bf19-3751ce4e6a1d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"535b99f0-d38e-4b9c-bf19-3751ce4e6a1d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"535b99f0-d38e-4b9c-bf19-3751ce4e6a1d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"535b99f0-d38e-4b9c-bf19-3751ce4e6a1d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"nikolay.nikolov@jiminny.com\"} {\"correlation_id\":\"535b99f0-d38e-4b9c-bf19-3751ce4e6a1d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"nikolay.nikolov@jiminny.com\"} {\"correlation_id\":\"535b99f0-d38e-4b9c-bf19-3751ce4e6a1d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"nikolay.nikolov@jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"535b99f0-d38e-4b9c-bf19-3751ce4e6a1d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":614381,\"team_id\":2,\"email\":\"nikolay.nikolov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"535b99f0-d38e-4b9c-bf19-3751ce4e6a1d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":614381,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"535b99f0-d38e-4b9c-bf19-3751ce4e6a1d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":614381} {\"correlation_id\":\"535b99f0-d38e-4b9c-bf19-3751ce4e6a1d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":614381,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"535b99f0-d38e-4b9c-bf19-3751ce4e6a1d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":614381,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"535b99f0-d38e-4b9c-bf19-3751ce4e6a1d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":614378,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":6167,\"account_id\":null,\"opportunity_id\":null,\"stage_id\":null}} {\"correlation_id\":\"f934b469-16df-4a4b-b5cc-bf918da87b87\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":614378} {\"correlation_id\":\"f934b469-16df-4a4b-b5cc-bf918da87b87\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":614378,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"f934b469-16df-4a4b-b5cc-bf918da87b87\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":614378,\"participants\":[{\"id\":1002623,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":1002624,\"user_id\":null,\"contact_id\":6167,\"lead_id\":null},{\"id\":1002625,\"user_id\":89,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"f934b469-16df-4a4b-b5cc-bf918da87b87\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"f934b469-16df-4a4b-b5cc-bf918da87b87\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"f934b469-16df-4a4b-b5cc-bf918da87b87\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"f934b469-16df-4a4b-b5cc-bf918da87b87\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"f934b469-16df-4a4b-b5cc-bf918da87b87\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"nikolay.nikolov@jiminny.com\"} {\"correlation_id\":\"f934b469-16df-4a4b-b5cc-bf918da87b87\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"nikolay.nikolov@jiminny.com\"} {\"correlation_id\":\"f934b469-16df-4a4b-b5cc-bf918da87b87\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"nikolay.nikolov@jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"f934b469-16df-4a4b-b5cc-bf918da87b87\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"nmalchev@gmail.com\"} {\"correlation_id\":\"f934b469-16df-4a4b-b5cc-bf918da87b87\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":614378,\"team_id\":2,\"email\":\"nikolay.nikolov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"f934b469-16df-4a4b-b5cc-bf918da87b87\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":614378,\"participants_processed\":3,\"exact_matches\":1,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"f934b469-16df-4a4b-b5cc-bf918da87b87\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":614378} {\"correlation_id\":\"f934b469-16df-4a4b-b5cc-bf918da87b87\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":614378,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"f934b469-16df-4a4b-b5cc-bf918da87b87\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":614378,\"remote_search\":true,\"lead_id\":null,\"contact_id\":6167,\"account_id\":null,\"opportunity_id\":null,\"stage_id\":null} {\"correlation_id\":\"f934b469-16df-4a4b-b5cc-bf918da87b87\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":613840,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36}} {\"correlation_id\":\"b440c986-63bd-445c-adfc-4d111801e63c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613840} {\"correlation_id\":\"b440c986-63bd-445c-adfc-4d111801e63c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613840,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"b440c986-63bd-445c-adfc-4d111801e63c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":613840,\"participants\":[{\"id\":1001764,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null},{\"id\":1001765,\"user_id\":261,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"b440c986-63bd-445c-adfc-4d111801e63c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"b440c986-63bd-445c-adfc-4d111801e63c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"b440c986-63bd-445c-adfc-4d111801e63c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"b440c986-63bd-445c-adfc-4d111801e63c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"b440c986-63bd-445c-adfc-4d111801e63c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: ProspectCache - Searching DB for opportunity by owner {\"account_id\":244,\"contact_id\":4487,\"owner_id\":261} {\"correlation_id\":\"b440c986-63bd-445c-adfc-4d111801e63c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: ProspectCache - Opportunity DB search results {\"account_id\":244,\"contact_id\":4487,\"opportunity_id\":299} {\"correlation_id\":\"b440c986-63bd-445c-adfc-4d111801e63c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"b440c986-63bd-445c-adfc-4d111801e63c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":613840,\"team_id\":2,\"email\":\"adelina.petrova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"b440c986-63bd-445c-adfc-4d111801e63c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":613840,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"b440c986-63bd-445c-adfc-4d111801e63c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613840} {\"correlation_id\":\"b440c986-63bd-445c-adfc-4d111801e63c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613840,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"b440c986-63bd-445c-adfc-4d111801e63c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":613840,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36} {\"correlation_id\":\"b440c986-63bd-445c-adfc-4d111801e63c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":613833,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36}} {\"correlation_id\":\"0360a22a-94dd-43cb-b57b-06bc159a05ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613833} {\"correlation_id\":\"0360a22a-94dd-43cb-b57b-06bc159a05ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613833,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"0360a22a-94dd-43cb-b57b-06bc159a05ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":613833,\"participants\":[{\"id\":1001750,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null},{\"id\":1001751,\"user_id\":261,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"0360a22a-94dd-43cb-b57b-06bc159a05ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"0360a22a-94dd-43cb-b57b-06bc159a05ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"0360a22a-94dd-43cb-b57b-06bc159a05ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"0360a22a-94dd-43cb-b57b-06bc159a05ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"0360a22a-94dd-43cb-b57b-06bc159a05ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"0360a22a-94dd-43cb-b57b-06bc159a05ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":613833,\"team_id\":2,\"email\":\"adelina.petrova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"0360a22a-94dd-43cb-b57b-06bc159a05ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":613833,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"0360a22a-94dd-43cb-b57b-06bc159a05ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613833} {\"correlation_id\":\"0360a22a-94dd-43cb-b57b-06bc159a05ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613833,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"0360a22a-94dd-43cb-b57b-06bc159a05ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":613833,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36} {\"correlation_id\":\"0360a22a-94dd-43cb-b57b-06bc159a05ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":613827,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36}} {\"correlation_id\":\"32fbe130-bf29-4e37-9c5a-bbe81f416a36\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613827} {\"correlation_id\":\"32fbe130-bf29-4e37-9c5a-bbe81f416a36\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613827,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"32fbe130-bf29-4e37-9c5a-bbe81f416a36\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":613827,\"participants\":[{\"id\":1001734,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null},{\"id\":1001735,\"user_id\":261,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"32fbe130-bf29-4e37-9c5a-bbe81f416a36\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"32fbe130-bf29-4e37-9c5a-bbe81f416a36\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"32fbe130-bf29-4e37-9c5a-bbe81f416a36\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"32fbe130-bf29-4e37-9c5a-bbe81f416a36\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"32fbe130-bf29-4e37-9c5a-bbe81f416a36\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"32fbe130-bf29-4e37-9c5a-bbe81f416a36\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":613827,\"team_id\":2,\"email\":\"adelina.petrova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"32fbe130-bf29-4e37-9c5a-bbe81f416a36\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":613827,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"32fbe130-bf29-4e37-9c5a-bbe81f416a36\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613827} {\"correlation_id\":\"32fbe130-bf29-4e37-9c5a-bbe81f416a36\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613827,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"32fbe130-bf29-4e37-9c5a-bbe81f416a36\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":613827,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36} {\"correlation_id\":\"32fbe130-bf29-4e37-9c5a-bbe81f416a36\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":613826,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36}} {\"correlation_id\":\"9b9716ff-ede9-4377-8ee8-b3c9a68b2dfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613826} {\"correlation_id\":\"9b9716ff-ede9-4377-8ee8-b3c9a68b2dfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613826,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"9b9716ff-ede9-4377-8ee8-b3c9a68b2dfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":613826,\"participants\":[{\"id\":1001732,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null},{\"id\":1001733,\"user_id\":261,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"9b9716ff-ede9-4377-8ee8-b3c9a68b2dfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"9b9716ff-ede9-4377-8ee8-b3c9a68b2dfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"9b9716ff-ede9-4377-8ee8-b3c9a68b2dfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"9b9716ff-ede9-4377-8ee8-b3c9a68b2dfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"9b9716ff-ede9-4377-8ee8-b3c9a68b2dfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"9b9716ff-ede9-4377-8ee8-b3c9a68b2dfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":613826,\"team_id\":2,\"email\":\"adelina.petrova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"9b9716ff-ede9-4377-8ee8-b3c9a68b2dfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":613826,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"9b9716ff-ede9-4377-8ee8-b3c9a68b2dfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613826} {\"correlation_id\":\"9b9716ff-ede9-4377-8ee8-b3c9a68b2dfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613826,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"9b9716ff-ede9-4377-8ee8-b3c9a68b2dfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":613826,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36} {\"correlation_id\":\"9b9716ff-ede9-4377-8ee8-b3c9a68b2dfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":613820,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36}} {\"correlation_id\":\"a131075c-6ae7-4912-92e1-adc33c291d1e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613820} {\"correlation_id\":\"a131075c-6ae7-4912-92e1-adc33c291d1e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613820,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"a131075c-6ae7-4912-92e1-adc33c291d1e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":613820,\"participants\":[{\"id\":1001721,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null},{\"id\":1001722,\"user_id\":261,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"a131075c-6ae7-4912-92e1-adc33c291d1e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"a131075c-6ae7-4912-92e1-adc33c291d1e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"a131075c-6ae7-4912-92e1-adc33c291d1e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"a131075c-6ae7-4912-92e1-adc33c291d1e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"a131075c-6ae7-4912-92e1-adc33c291d1e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"a131075c-6ae7-4912-92e1-adc33c291d1e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":613820,\"team_id\":2,\"email\":\"adelina.petrova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"a131075c-6ae7-4912-92e1-adc33c291d1e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":613820,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"a131075c-6ae7-4912-92e1-adc33c291d1e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613820} {\"correlation_id\":\"a131075c-6ae7-4912-92e1-adc33c291d1e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613820,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"a131075c-6ae7-4912-92e1-adc33c291d1e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":613820,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36} {\"correlation_id\":\"a131075c-6ae7-4912-92e1-adc33c291d1e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":613818,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36}} {\"correlation_id\":\"d36e09b8-5b15-40f2-8fa0-0beb3f0ebe69\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613818} {\"correlation_id\":\"d36e09b8-5b15-40f2-8fa0-0beb3f0ebe69\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613818,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"d36e09b8-5b15-40f2-8fa0-0beb3f0ebe69\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":613818,\"participants\":[{\"id\":1001717,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null},{\"id\":1001718,\"user_id\":261,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"d36e09b8-5b15-40f2-8fa0-0beb3f0ebe69\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"d36e09b8-5b15-40f2-8fa0-0beb3f0ebe69\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"d36e09b8-5b15-40f2-8fa0-0beb3f0ebe69\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d36e09b8-5b15-40f2-8fa0-0beb3f0ebe69\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"d36e09b8-5b15-40f2-8fa0-0beb3f0ebe69\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"d36e09b8-5b15-40f2-8fa0-0beb3f0ebe69\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":613818,\"team_id\":2,\"email\":\"adelina.petrova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"d36e09b8-5b15-40f2-8fa0-0beb3f0ebe69\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":613818,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"d36e09b8-5b15-40f2-8fa0-0beb3f0ebe69\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613818} {\"correlation_id\":\"d36e09b8-5b15-40f2-8fa0-0beb3f0ebe69\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613818,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"d36e09b8-5b15-40f2-8fa0-0beb3f0ebe69\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":613818,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36} {\"correlation_id\":\"d36e09b8-5b15-40f2-8fa0-0beb3f0ebe69\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":613812,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36}} {\"correlation_id\":\"9e5ea20b-def9-4ace-8a73-9d08a04cc6d9\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613812} {\"correlation_id\":\"9e5ea20b-def9-4ace-8a73-9d08a04cc6d9\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613812,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"9e5ea20b-def9-4ace-8a73-9d08a04cc6d9\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":613812,\"participants\":[{\"id\":1001705,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null},{\"id\":1001706,\"user_id\":261,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"9e5ea20b-def9-4ace-8a73-9d08a04cc6d9\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"9e5ea20b-def9-4ace-8a73-9d08a04cc6d9\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"9e5ea20b-def9-4ace-8a73-9d08a04cc6d9\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"9e5ea20b-def9-4ace-8a73-9d08a04cc6d9\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"9e5ea20b-def9-4ace-8a73-9d08a04cc6d9\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"9e5ea20b-def9-4ace-8a73-9d08a04cc6d9\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":613812,\"team_id\":2,\"email\":\"adelina.petrova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"9e5ea20b-def9-4ace-8a73-9d08a04cc6d9\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":613812,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"9e5ea20b-def9-4ace-8a73-9d08a04cc6d9\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613812} {\"correlation_id\":\"9e5ea20b-def9-4ace-8a73-9d08a04cc6d9\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613812,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"9e5ea20b-def9-4ace-8a73-9d08a04cc6d9\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":613812,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36} {\"correlation_id\":\"9e5ea20b-def9-4ace-8a73-9d08a04cc6d9\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":613807,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4484,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"be53e876-a319-40dc-a89b-0728d9a47c6e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613807} {\"correlation_id\":\"be53e876-a319-40dc-a89b-0728d9a47c6e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613807,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"be53e876-a319-40dc-a89b-0728d9a47c6e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":613807,\"participants\":[{\"id\":1001690,\"user_id\":253,\"contact_id\":null,\"lead_id\":null},{\"id\":1001691,\"user_id\":null,\"contact_id\":4484,\"lead_id\":null}]} {\"correlation_id\":\"be53e876-a319-40dc-a89b-0728d9a47c6e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"be53e876-a319-40dc-a89b-0728d9a47c6e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"be53e876-a319-40dc-a89b-0728d9a47c6e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"be53e876-a319-40dc-a89b-0728d9a47c6e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"be53e876-a319-40dc-a89b-0728d9a47c6e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":613807,\"team_id\":2,\"email\":\"preslava.ivanova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"be53e876-a319-40dc-a89b-0728d9a47c6e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: ProspectCache - Searching DB for opportunity by owner {\"account_id\":243,\"contact_id\":4484,\"owner_id\":253} {\"correlation_id\":\"be53e876-a319-40dc-a89b-0728d9a47c6e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: ProspectCache - Fallback DB opportunity search {\"account_id\":243,\"contact_id\":4484} {\"correlation_id\":\"be53e876-a319-40dc-a89b-0728d9a47c6e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: ProspectCache - Opportunity DB search results {\"account_id\":243,\"contact_id\":4484,\"opportunity_id\":276} {\"correlation_id\":\"be53e876-a319-40dc-a89b-0728d9a47c6e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"tsvetomir.banovski@gmail.com\"} {\"correlation_id\":\"be53e876-a319-40dc-a89b-0728d9a47c6e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":613807,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"be53e876-a319-40dc-a89b-0728d9a47c6e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613807} {\"correlation_id\":\"be53e876-a319-40dc-a89b-0728d9a47c6e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613807,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"be53e876-a319-40dc-a89b-0728d9a47c6e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":613807,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4484,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"be53e876-a319-40dc-a89b-0728d9a47c6e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":613806,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":350,\"stage_id\":34}} {\"correlation_id\":\"94c53200-4779-41ab-82ea-571c1bcdd8b7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613806} {\"correlation_id\":\"94c53200-4779-41ab-82ea-571c1bcdd8b7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613806,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"94c53200-4779-41ab-82ea-571c1bcdd8b7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":613806,\"participants\":[{\"id\":1001688,\"user_id\":253,\"contact_id\":null,\"lead_id\":null},{\"id\":1001689,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null}]} {\"correlation_id\":\"94c53200-4779-41ab-82ea-571c1bcdd8b7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"94c53200-4779-41ab-82ea-571c1bcdd8b7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"94c53200-4779-41ab-82ea-571c1bcdd8b7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"94c53200-4779-41ab-82ea-571c1bcdd8b7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"94c53200-4779-41ab-82ea-571c1bcdd8b7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":613806,\"team_id\":2,\"email\":\"preslava.ivanova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"94c53200-4779-41ab-82ea-571c1bcdd8b7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: ProspectCache - Searching DB for opportunity by owner {\"account_id\":244,\"contact_id\":4487,\"owner_id\":253} {\"correlation_id\":\"94c53200-4779-41ab-82ea-571c1bcdd8b7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: ProspectCache - Fallback DB opportunity search {\"account_id\":244,\"contact_id\":4487} {\"correlation_id\":\"94c53200-4779-41ab-82ea-571c1bcdd8b7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: ProspectCache - Opportunity DB search results {\"account_id\":244,\"contact_id\":4487,\"opportunity_id\":350} {\"correlation_id\":\"94c53200-4779-41ab-82ea-571c1bcdd8b7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"94c53200-4779-41ab-82ea-571c1bcdd8b7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":613806,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"94c53200-4779-41ab-82ea-571c1bcdd8b7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613806} {\"correlation_id\":\"94c53200-4779-41ab-82ea-571c1bcdd8b7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613806,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"94c53200-4779-41ab-82ea-571c1bcdd8b7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":613806,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":350,\"stage_id\":34} {\"correlation_id\":\"94c53200-4779-41ab-82ea-571c1bcdd8b7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":613805,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":350,\"stage_id\":34}} {\"correlation_id\":\"d1a190ef-9b03-49ff-ad47-50afa53ef345\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613805} {\"correlation_id\":\"d1a190ef-9b03-49ff-ad47-50afa53ef345\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613805,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"d1a190ef-9b03-49ff-ad47-50afa53ef345\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":613805,\"participants\":[{\"id\":1001686,\"user_id\":253,\"contact_id\":null,\"lead_id\":null},{\"id\":1001687,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null}]} {\"correlation_id\":\"d1a190ef-9b03-49ff-ad47-50afa53ef345\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"d1a190ef-9b03-49ff-ad47-50afa53ef345\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"d1a190ef-9b03-49ff-ad47-50afa53ef345\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d1a190ef-9b03-49ff-ad47-50afa53ef345\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"d1a190ef-9b03-49ff-ad47-50afa53ef345\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":613805,\"team_id\":2,\"email\":\"preslava.ivanova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"d1a190ef-9b03-49ff-ad47-50afa53ef345\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"d1a190ef-9b03-49ff-ad47-50afa53ef345\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":613805,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"d1a190ef-9b03-49ff-ad47-50afa53ef345\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613805} {\"correlation_id\":\"d1a190ef-9b03-49ff-ad47-50afa53ef345\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613805,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"d1a190ef-9b03-49ff-ad47-50afa53ef345\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":613805,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":350,\"stage_id\":34} {\"correlation_id\":\"d1a190ef-9b03-49ff-ad47-50afa53ef345\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":613698,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36}} {\"correlation_id\":\"2fa5fec1-54fb-40cc-93d4-145712a9d2fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613698} {\"correlation_id\":\"2fa5fec1-54fb-40cc-93d4-145712a9d2fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613698,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"2fa5fec1-54fb-40cc-93d4-145712a9d2fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":613698,\"participants\":[{\"id\":1001667,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null},{\"id\":1001668,\"user_id\":261,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"2fa5fec1-54fb-40cc-93d4-145712a9d2fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"2fa5fec1-54fb-40cc-93d4-145712a9d2fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"2fa5fec1-54fb-40cc-93d4-145712a9d2fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"2fa5fec1-54fb-40cc-93d4-145712a9d2fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"2fa5fec1-54fb-40cc-93d4-145712a9d2fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"2fa5fec1-54fb-40cc-93d4-145712a9d2fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":613698,\"team_id\":2,\"email\":\"adelina.petrova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"2fa5fec1-54fb-40cc-93d4-145712a9d2fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":613698,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"2fa5fec1-54fb-40cc-93d4-145712a9d2fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613698} {\"correlation_id\":\"2fa5fec1-54fb-40cc-93d4-145712a9d2fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613698,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"2fa5fec1-54fb-40cc-93d4-145712a9d2fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":613698,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36} {\"correlation_id\":\"2fa5fec1-54fb-40cc-93d4-145712a9d2fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":613697,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36}} {\"correlation_id\":\"4da249c2-1056-47f9-869a-53c5fda5b9ef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613697} {\"correlation_id\":\"4da249c2-1056-47f9-869a-53c5fda5b9ef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613697,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"4da249c2-1056-47f9-869a-53c5fda5b9ef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":613697,\"participants\":[{\"id\":1001665,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null},{\"id\":1001666,\"user_id\":261,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"4da249c2-1056-47f9-869a-53c5fda5b9ef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"4da249c2-1056-47f9-869a-53c5fda5b9ef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"4da249c2-1056-47f9-869a-53c5fda5b9ef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4da249c2-1056-47f9-869a-53c5fda5b9ef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"4da249c2-1056-47f9-869a-53c5fda5b9ef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"4da249c2-1056-47f9-869a-53c5fda5b9ef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":613697,\"team_id\":2,\"email\":\"adelina.petrova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"4da249c2-1056-47f9-869a-53c5fda5b9ef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":613697,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"4da249c2-1056-47f9-869a-53c5fda5b9ef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613697} {\"correlation_id\":\"4da249c2-1056-47f9-869a-53c5fda5b9ef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613697,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"4da249c2-1056-47f9-869a-53c5fda5b9ef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":613697,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36} {\"correlation_id\":\"4da249c2-1056-47f9-869a-53c5fda5b9ef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":613696,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36}} {\"correlation_id\":\"8224abca-8e96-4a39-8baa-b4bc4f746e1b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613696} {\"correlation_id\":\"8224abca-8e96-4a39-8baa-b4bc4f746e1b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613696,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"8224abca-8e96-4a39-8baa-b4bc4f746e1b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":613696,\"participants\":[{\"id\":1001663,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null},{\"id\":1001664,\"user_id\":261,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"8224abca-8e96-4a39-8baa-b4bc4f746e1b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"8224abca-8e96-4a39-8baa-b4bc4f746e1b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"8224abca-8e96-4a39-8baa-b4bc4f746e1b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"8224abca-8e96-4a39-8baa-b4bc4f746e1b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"8224abca-8e96-4a39-8baa-b4bc4f746e1b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"8224abca-8e96-4a39-8baa-b4bc4f746e1b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":613696,\"team_id\":2,\"email\":\"adelina.petrova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"8224abca-8e96-4a39-8baa-b4bc4f746e1b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":613696,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"8224abca-8e96-4a39-8baa-b4bc4f746e1b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613696} {\"correlation_id\":\"8224abca-8e96-4a39-8baa-b4bc4f746e1b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613696,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"8224abca-8e96-4a39-8baa-b4bc4f746e1b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":613696,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36} {\"correlation_id\":\"8224abca-8e96-4a39-8baa-b4bc4f746e1b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":613695,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36}} {\"correlation_id\":\"e6c53892-ee7f-42df-ae80-06e4cab7eeea\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613695} {\"correlation_id\":\"e6c53892-ee7f-42df-ae80-06e4cab7eeea\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613695,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"e6c53892-ee7f-42df-ae80-06e4cab7eeea\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":613695,\"participants\":[{\"id\":1001661,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null},{\"id\":1001662,\"user_id\":261,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"e6c53892-ee7f-42df-ae80-06e4cab7eeea\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"e6c53892-ee7f-42df-ae80-06e4cab7eeea\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"e6c53892-ee7f-42df-ae80-06e4cab7eeea\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"e6c53892-ee7f-42df-ae80-06e4cab7eeea\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"e6c53892-ee7f-42df-ae80-06e4cab7eeea\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"e6c53892-ee7f-42df-ae80-06e4cab7eeea\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":613695,\"team_id\":2,\"email\":\"adelina.petrova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"e6c53892-ee7f-42df-ae80-06e4cab7eeea\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":613695,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"e6c53892-ee7f-42df-ae80-06e4cab7eeea\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613695} {\"correlation_id\":\"e6c53892-ee7f-42df-ae80-06e4cab7eeea\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613695,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"e6c53892-ee7f-42df-ae80-06e4cab7eeea\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":613695,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36} {\"correlation_id\":\"e6c53892-ee7f-42df-ae80-06e4cab7eeea\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.NOTICE: Monitoring start {\"correlation_id\":\"2766b9f0-0c60-4d94-9b69-4f2dc977fe59\",\"trace_id\":\"0810b004-42ab-49a4-b3bf-4fbda4a819fe\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":613694,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36}} {\"correlation_id\":\"34f7f8f5-b1b3-4176-bc4a-f96265db81c5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613694} {\"correlation_id\":\"34f7f8f5-b1b3-4176-bc4a-f96265db81c5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613694,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"34f7f8f5-b1b3-4176-bc4a-f96265db81c5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.NOTICE: Monitoring end {\"correlation_id\":\"2766b9f0-0c60-4d94-9b69-4f2dc977fe59\",\"trace_id\":\"0810b004-42ab-49a4-b3bf-4fbda4a819fe\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":613694,\"participants\":[{\"id\":1001659,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null},{\"id\":1001660,\"user_id\":261,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"34f7f8f5-b1b3-4176-bc4a-f96265db81c5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"34f7f8f5-b1b3-4176-bc4a-f96265db81c5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"34f7f8f5-b1b3-4176-bc4a-f96265db81c5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"34f7f8f5-b1b3-4176-bc4a-f96265db81c5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"34f7f8f5-b1b3-4176-bc4a-f96265db81c5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"34f7f8f5-b1b3-4176-bc4a-f96265db81c5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":613694,\"team_id\":2,\"email\":\"adelina.petrova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"34f7f8f5-b1b3-4176-bc4a-f96265db81c5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":613694,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"34f7f8f5-b1b3-4176-bc4a-f96265db81c5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613694} {\"correlation_id\":\"34f7f8f5-b1b3-4176-bc4a-f96265db81c5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613694,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"34f7f8f5-b1b3-4176-bc4a-f96265db81c5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":613694,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36} {\"correlation_id\":\"34f7f8f5-b1b3-4176-bc4a-f96265db81c5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":613157,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":350,\"stage_id\":34}} {\"correlation_id\":\"38fd2794-89d3-4ce9-a9fc-f9f8382e30d5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613157} {\"correlation_id\":\"38fd2794-89d3-4ce9-a9fc-f9f8382e30d5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613157,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"38fd2794-89d3-4ce9-a9fc-f9f8382e30d5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":613157,\"participants\":[{\"id\":1000746,\"user_id\":253,\"contact_id\":null,\"lead_id\":null},{\"id\":1000747,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null}]} {\"correlation_id\":\"38fd2794-89d3-4ce9-a9fc-f9f8382e30d5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"38fd2794-89d3-4ce9-a9fc-f9f8382e30d5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"38fd2794-89d3-4ce9-a9fc-f9f8382e30d5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"38fd2794-89d3-4ce9-a9fc-f9f8382e30d5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"38fd2794-89d3-4ce9-a9fc-f9f8382e30d5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":613157,\"team_id\":2,\"email\":\"preslava.ivanova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"38fd2794-89d3-4ce9-a9fc-f9f8382e30d5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"38fd2794-89d3-4ce9-a9fc-f9f8382e30d5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":613157,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"38fd2794-89d3-4ce9-a9fc-f9f8382e30d5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613157} {\"correlation_id\":\"38fd2794-89d3-4ce9-a9fc-f9f8382e30d5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613157,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"38fd2794-89d3-4ce9-a9fc-f9f8382e30d5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":613157,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":350,\"stage_id\":34} {\"correlation_id\":\"38fd2794-89d3-4ce9-a9fc-f9f8382e30d5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":613156,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":350,\"stage_id\":34}} {\"correlation_id\":\"f33d171a-96c9-465c-bcf2-facecea0a7d6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613156} {\"correlation_id\":\"f33d171a-96c9-465c-bcf2-facecea0a7d6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613156,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"f33d171a-96c9-465c-bcf2-facecea0a7d6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":613156,\"participants\":[{\"id\":1000744,\"user_id\":253,\"contact_id\":null,\"lead_id\":null},{\"id\":1000745,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null}]} {\"correlation_id\":\"f33d171a-96c9-465c-bcf2-facecea0a7d6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"f33d171a-96c9-465c-bcf2-facecea0a7d6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"f33d171a-96c9-465c-bcf2-facecea0a7d6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"f33d171a-96c9-465c-bcf2-facecea0a7d6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"f33d171a-96c9-465c-bcf2-facecea0a7d6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":613156,\"team_id\":2,\"email\":\"preslava.ivanova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"f33d171a-96c9-465c-bcf2-facecea0a7d6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"f33d171a-96c9-465c-bcf2-facecea0a7d6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":613156,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"f33d171a-96c9-465c-bcf2-facecea0a7d6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613156} {\"correlation_id\":\"f33d171a-96c9-465c-bcf2-facecea0a7d6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613156,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"f33d171a-96c9-465c-bcf2-facecea0a7d6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":613156,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":350,\"stage_id\":34} {\"correlation_id\":\"f33d171a-96c9-465c-bcf2-facecea0a7d6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":613155,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":350,\"stage_id\":34}} {\"correlation_id\":\"6907203d-5426-446b-b0ed-cb3a79828876\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613155} {\"correlation_id\":\"6907203d-5426-446b-b0ed-cb3a79828876\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613155,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"6907203d-5426-446b-b0ed-cb3a79828876\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":613155,\"participants\":[{\"id\":1000742,\"user_id\":253,\"contact_id\":null,\"lead_id\":null},{\"id\":1000743,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null}]} {\"correlation_id\":\"6907203d-5426-446b-b0ed-cb3a79828876\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"6907203d-5426-446b-b0ed-cb3a79828876\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"6907203d-5426-446b-b0ed-cb3a79828876\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"6907203d-5426-446b-b0ed-cb3a79828876\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"6907203d-5426-446b-b0ed-cb3a79828876\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":613155,\"team_id\":2,\"email\":\"preslava.ivanova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"6907203d-5426-446b-b0ed-cb3a79828876\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"6907203d-5426-446b-b0ed-cb3a79828876\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":613155,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"6907203d-5426-446b-b0ed-cb3a79828876\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613155} {\"correlation_id\":\"6907203d-5426-446b-b0ed-cb3a79828876\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613155,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"6907203d-5426-446b-b0ed-cb3a79828876\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":613155,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":350,\"stage_id\":34} {\"correlation_id\":\"6907203d-5426-446b-b0ed-cb3a79828876\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":613130,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36}} {\"correlation_id\":\"b39eb37f-ecba-45cf-99d3-dc45ee004976\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613130} {\"correlation_id\":\"b39eb37f-ecba-45cf-99d3-dc45ee004976\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613130,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"b39eb37f-ecba-45cf-99d3-dc45ee004976\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":613130,\"participants\":[{\"id\":1000693,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null},{\"id\":1000694,\"user_id\":261,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"b39eb37f-ecba-45cf-99d3-dc45ee004976\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"b39eb37f-ecba-45cf-99d3-dc45ee004976\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"b39eb37f-ecba-45cf-99d3-dc45ee004976\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"b39eb37f-ecba-45cf-99d3-dc45ee004976\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"b39eb37f-ecba-45cf-99d3-dc45ee004976\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"b39eb37f-ecba-45cf-99d3-dc45ee004976\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":613130,\"team_id\":2,\"email\":\"adelina.petrova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"b39eb37f-ecba-45cf-99d3-dc45ee004976\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":613130,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"b39eb37f-ecba-45cf-99d3-dc45ee004976\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613130} {\"correlation_id\":\"b39eb37f-ecba-45cf-99d3-dc45ee004976\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613130,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"b39eb37f-ecba-45cf-99d3-dc45ee004976\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":613130,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36} {\"correlation_id\":\"b39eb37f-ecba-45cf-99d3-dc45ee004976\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612924,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":165,\"stage_id\":89}} {\"correlation_id\":\"22353f3a-e0aa-43eb-9a06-f618d38746e7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612924} {\"correlation_id\":\"22353f3a-e0aa-43eb-9a06-f618d38746e7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612924,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"22353f3a-e0aa-43eb-9a06-f618d38746e7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612924,\"participants\":[{\"id\":1000290,\"user_id\":19,\"contact_id\":null,\"lead_id\":null},{\"id\":1000291,\"user_id\":null,\"contact_id\":97,\"lead_id\":null}]} {\"correlation_id\":\"22353f3a-e0aa-43eb-9a06-f618d38746e7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"22353f3a-e0aa-43eb-9a06-f618d38746e7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"22353f3a-e0aa-43eb-9a06-f618d38746e7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"22353f3a-e0aa-43eb-9a06-f618d38746e7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"22353f3a-e0aa-43eb-9a06-f618d38746e7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612924,\"team_id\":2,\"email\":\"james.graham@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"22353f3a-e0aa-43eb-9a06-f618d38746e7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: ProspectCache - Searching DB for opportunity by owner {\"account_id\":69,\"contact_id\":97,\"owner_id\":19} {\"correlation_id\":\"22353f3a-e0aa-43eb-9a06-f618d38746e7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: ProspectCache - Opportunity DB search results {\"account_id\":69,\"contact_id\":97,\"opportunity_id\":165} {\"correlation_id\":\"22353f3a-e0aa-43eb-9a06-f618d38746e7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinsoncrusoe@test.com\"} {\"correlation_id\":\"22353f3a-e0aa-43eb-9a06-f618d38746e7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612924,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"22353f3a-e0aa-43eb-9a06-f618d38746e7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612924} {\"correlation_id\":\"22353f3a-e0aa-43eb-9a06-f618d38746e7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612924,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"22353f3a-e0aa-43eb-9a06-f618d38746e7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612924,\"remote_search\":true,\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":165,\"stage_id\":89} {\"correlation_id\":\"22353f3a-e0aa-43eb-9a06-f618d38746e7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612923,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":165,\"stage_id\":89}} {\"correlation_id\":\"602cc3c5-932c-4d3d-9370-35217adac6e0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612923} {\"correlation_id\":\"602cc3c5-932c-4d3d-9370-35217adac6e0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612923,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"602cc3c5-932c-4d3d-9370-35217adac6e0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612923,\"participants\":[{\"id\":1000288,\"user_id\":19,\"contact_id\":null,\"lead_id\":null},{\"id\":1000289,\"user_id\":null,\"contact_id\":97,\"lead_id\":null}]} {\"correlation_id\":\"602cc3c5-932c-4d3d-9370-35217adac6e0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"602cc3c5-932c-4d3d-9370-35217adac6e0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"602cc3c5-932c-4d3d-9370-35217adac6e0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"602cc3c5-932c-4d3d-9370-35217adac6e0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"602cc3c5-932c-4d3d-9370-35217adac6e0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612923,\"team_id\":2,\"email\":\"james.graham@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"602cc3c5-932c-4d3d-9370-35217adac6e0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinsoncrusoe@test.com\"} {\"correlation_id\":\"602cc3c5-932c-4d3d-9370-35217adac6e0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612923,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"602cc3c5-932c-4d3d-9370-35217adac6e0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612923} {\"correlation_id\":\"602cc3c5-932c-4d3d-9370-35217adac6e0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612923,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"602cc3c5-932c-4d3d-9370-35217adac6e0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612923,\"remote_search\":true,\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":165,\"stage_id\":89} {\"correlation_id\":\"602cc3c5-932c-4d3d-9370-35217adac6e0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612922,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":165,\"stage_id\":89}} {\"correlation_id\":\"4272a600-7709-4e0b-9ddd-2bf9a93ee051\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612922} {\"correlation_id\":\"4272a600-7709-4e0b-9ddd-2bf9a93ee051\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612922,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"4272a600-7709-4e0b-9ddd-2bf9a93ee051\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612922,\"participants\":[{\"id\":1000286,\"user_id\":19,\"contact_id\":null,\"lead_id\":null},{\"id\":1000287,\"user_id\":null,\"contact_id\":97,\"lead_id\":null}]} {\"correlation_id\":\"4272a600-7709-4e0b-9ddd-2bf9a93ee051\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"4272a600-7709-4e0b-9ddd-2bf9a93ee051\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"4272a600-7709-4e0b-9ddd-2bf9a93ee051\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4272a600-7709-4e0b-9ddd-2bf9a93ee051\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"4272a600-7709-4e0b-9ddd-2bf9a93ee051\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612922,\"team_id\":2,\"email\":\"james.graham@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"4272a600-7709-4e0b-9ddd-2bf9a93ee051\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinsoncrusoe@test.com\"} {\"correlation_id\":\"4272a600-7709-4e0b-9ddd-2bf9a93ee051\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612922,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"4272a600-7709-4e0b-9ddd-2bf9a93ee051\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612922} {\"correlation_id\":\"4272a600-7709-4e0b-9ddd-2bf9a93ee051\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612922,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"4272a600-7709-4e0b-9ddd-2bf9a93ee051\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612922,\"remote_search\":true,\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":165,\"stage_id\":89} {\"correlation_id\":\"4272a600-7709-4e0b-9ddd-2bf9a93ee051\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612847,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36}} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612847} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612847,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612847,\"participants\":[{\"id\":1000130,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":1000131,\"user_id\":261,\"contact_id\":null,\"lead_id\":null},{\"id\":1000151,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null}]} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [Prospect match] Cache miss, calling the API {\"identifier_type\":\"email\",\"identifier\":\"adelina.petrova@jiminny.com\"} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [Hubspot] Failed to fetch contact {\"email\":\"adelina.petrova@jiminny.com\",\"reason\":\"[404] Client error: `GET https://api.hubapi.com/crm/v3/objects/contacts/adelina.petrova%40jiminny.com?properties=email%2Cfirstname%2Clastname%2Ccountry%2Cphone%2Cmobilephone%2Cjobtitle%2Chubspot_owner_id%2Cassociatedcompanyid%2Cphoto&archived=0&idProperty=email` resulted in a `404 Not Found` response\"} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [Prospect match] API returned empty result, caching the miss with empty prospect data {\"identifier_type\":\"email\",\"identifier\":\"adelina.petrova@jiminny.com\"} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:27] local.INFO: [Hubspot] Pagination completed {\"team_id\":2,\"endpoint\":\"https://api.hubapi.com/crm/v3/objects/contact/search\",\"total_requests\":1,\"total_records_fetched\":0,\"total_elapsed_seconds\":0.22,\"average_seconds_per_request\":0.22} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:27] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"adelina.petrova@jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:27] local.INFO: [Prospect match] Cache miss {\"identifier_type\":\"domain\",\"identifier\":\"jiminny.com\",\"crm\":\"hubspot\"} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:27] local.INFO: [Prospect match] Cache miss, calling the API {\"identifier_type\":\"domain\",\"identifier\":\"jiminny.com\"} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:27] local.INFO: [HubSpot] importAccount {\"crm_provider_id\":\"749766179\",\"config_id\":2} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:27] local.INFO: [HubSpot] CRM Search requested {\"request\":{\"filterGroups\":[{\"filters\":[{\"propertyName\":\"associations.company\",\"operator\":\"EQ\",\"value\":\"749766179\"},{\"propertyName\":\"dealstage\",\"operator\":\"NOT_IN\",\"values\":[\"closedwon\",\"4040964\",\"59247967\"]},{\"propertyName\":\"dealstage\",\"operator\":\"NOT_IN\",\"values\":[\"closedlost\",\"4040965\",\"59247968\"]}]}],\"sorts\":[{\"propertyName\":\"modifieddate\",\"direction\":\"DESCENDING\"}],\"properties\":[\"dealname\",\"amount\",\"hubspot_owner_id\",\"pipeline\",\"dealstage\",\"closedate\",\"deal_currency_code\"],\"limit\":200}} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:27] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"6bdeb8f0-c361-4cb3-9fb1-c6bdf466a73e\",\"trace_id\":\"d050352e-9bf5-497a-99d6-e46661a04a04\"}\n[2026-05-11 10:43:27] local.INFO: [Hubspot] Pagination completed {\"team_id\":2,\"endpoint\":\"https://api.hubapi.com/crm/v3/objects/deals/search\",\"total_requests\":1,\"total_records_fetched\":10,\"total_elapsed_seconds\":0.25,\"average_seconds_per_request\":0.25} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:27] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":60.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"6bdeb8f0-c361-4cb3-9fb1-c6bdf466a73e\",\"trace_id\":\"d050352e-9bf5-497a-99d6-e46661a04a04\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612847,\"team_id\":2,\"email\":\"adelina.petrova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612847,\"participants_processed\":3,\"exact_matches\":1,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612847} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612847,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612847,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612822,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36}} {\"correlation_id\":\"a8970854-ad6a-4751-9cc5-ffe651d20f06\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612822} {\"correlation_id\":\"a8970854-ad6a-4751-9cc5-ffe651d20f06\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612822,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"a8970854-ad6a-4751-9cc5-ffe651d20f06\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612822,\"participants\":[{\"id\":1000080,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null},{\"id\":1000081,\"user_id\":261,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"a8970854-ad6a-4751-9cc5-ffe651d20f06\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"a8970854-ad6a-4751-9cc5-ffe651d20f06\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"a8970854-ad6a-4751-9cc5-ffe651d20f06\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"a8970854-ad6a-4751-9cc5-ffe651d20f06\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"a8970854-ad6a-4751-9cc5-ffe651d20f06\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"a8970854-ad6a-4751-9cc5-ffe651d20f06\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612822,\"team_id\":2,\"email\":\"adelina.petrova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"a8970854-ad6a-4751-9cc5-ffe651d20f06\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612822,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"a8970854-ad6a-4751-9cc5-ffe651d20f06\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612822} {\"correlation_id\":\"a8970854-ad6a-4751-9cc5-ffe651d20f06\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612822,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"a8970854-ad6a-4751-9cc5-ffe651d20f06\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612822,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36} {\"correlation_id\":\"a8970854-ad6a-4751-9cc5-ffe651d20f06\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612819,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36}} {\"correlation_id\":\"16e6bc16-5ab8-49ce-b7da-c292b8c94105\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612819} {\"correlation_id\":\"16e6bc16-5ab8-49ce-b7da-c292b8c94105\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612819,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"16e6bc16-5ab8-49ce-b7da-c292b8c94105\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612819,\"participants\":[{\"id\":1000073,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null},{\"id\":1000074,\"user_id\":261,\"contact_id\":null,\"lead_id\":null},{\"id\":1000075,\"user_id\":null,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"16e6bc16-5ab8-49ce-b7da-c292b8c94105\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"16e6bc16-5ab8-49ce-b7da-c292b8c94105\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"16e6bc16-5ab8-49ce-b7da-c292b8c94105\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"16e6bc16-5ab8-49ce-b7da-c292b8c94105\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"16e6bc16-5ab8-49ce-b7da-c292b8c94105\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"16e6bc16-5ab8-49ce-b7da-c292b8c94105\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612819,\"team_id\":2,\"email\":\"adelina.petrova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"16e6bc16-5ab8-49ce-b7da-c292b8c94105\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"adelina.petrova@jiminny.com\"} {\"correlation_id\":\"16e6bc16-5ab8-49ce-b7da-c292b8c94105\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"adelina.petrova@jiminny.com\"} {\"correlation_id\":\"16e6bc16-5ab8-49ce-b7da-c292b8c94105\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"adelina.petrova@jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"16e6bc16-5ab8-49ce-b7da-c292b8c94105\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612819,\"participants_processed\":3,\"exact_matches\":1,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"16e6bc16-5ab8-49ce-b7da-c292b8c94105\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612819} {\"correlation_id\":\"16e6bc16-5ab8-49ce-b7da-c292b8c94105\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612819,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"16e6bc16-5ab8-49ce-b7da-c292b8c94105\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612819,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36} {\"correlation_id\":\"16e6bc16-5ab8-49ce-b7da-c292b8c94105\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612673,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36}} {\"correlation_id\":\"8711a017-62dd-479f-b84e-123d6e3f40be\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612673} {\"correlation_id\":\"8711a017-62dd-479f-b84e-123d6e3f40be\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612673,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"8711a017-62dd-479f-b84e-123d6e3f40be\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612673,\"participants\":[{\"id\":999993,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null},{\"id\":999994,\"user_id\":261,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"8711a017-62dd-479f-b84e-123d6e3f40be\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"8711a017-62dd-479f-b84e-123d6e3f40be\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"8711a017-62dd-479f-b84e-123d6e3f40be\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"8711a017-62dd-479f-b84e-123d6e3f40be\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"8711a017-62dd-479f-b84e-123d6e3f40be\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"8711a017-62dd-479f-b84e-123d6e3f40be\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612673,\"team_id\":2,\"email\":\"adelina.petrova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"8711a017-62dd-479f-b84e-123d6e3f40be\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612673,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"8711a017-62dd-479f-b84e-123d6e3f40be\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612673} {\"correlation_id\":\"8711a017-62dd-479f-b84e-123d6e3f40be\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612673,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"8711a017-62dd-479f-b84e-123d6e3f40be\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612673,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36} {\"correlation_id\":\"8711a017-62dd-479f-b84e-123d6e3f40be\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612642,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36}} {\"correlation_id\":\"aa7b8829-1739-4f9b-aad1-c6868d8679ca\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612642} {\"correlation_id\":\"aa7b8829-1739-4f9b-aad1-c6868d8679ca\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612642,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"aa7b8829-1739-4f9b-aad1-c6868d8679ca\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612642,\"participants\":[{\"id\":999935,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null},{\"id\":999936,\"user_id\":261,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"aa7b8829-1739-4f9b-aad1-c6868d8679ca\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"aa7b8829-1739-4f9b-aad1-c6868d8679ca\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"aa7b8829-1739-4f9b-aad1-c6868d8679ca\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"aa7b8829-1739-4f9b-aad1-c6868d8679ca\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"aa7b8829-1739-4f9b-aad1-c6868d8679ca\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"aa7b8829-1739-4f9b-aad1-c6868d8679ca\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612642,\"team_id\":2,\"email\":\"adelina.petrova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"aa7b8829-1739-4f9b-aad1-c6868d8679ca\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612642,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"aa7b8829-1739-4f9b-aad1-c6868d8679ca\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612642} {\"correlation_id\":\"aa7b8829-1739-4f9b-aad1-c6868d8679ca\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612642,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"aa7b8829-1739-4f9b-aad1-c6868d8679ca\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612642,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36} {\"correlation_id\":\"aa7b8829-1739-4f9b-aad1-c6868d8679ca\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612598,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"2fef3119-c9e0-421d-b77e-3cf1e26b480a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612598} {\"correlation_id\":\"2fef3119-c9e0-421d-b77e-3cf1e26b480a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612598,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"2fef3119-c9e0-421d-b77e-3cf1e26b480a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612598,\"participants\":[{\"id\":999857,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null},{\"id\":999858,\"user_id\":206,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"2fef3119-c9e0-421d-b77e-3cf1e26b480a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"2fef3119-c9e0-421d-b77e-3cf1e26b480a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"2fef3119-c9e0-421d-b77e-3cf1e26b480a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"2fef3119-c9e0-421d-b77e-3cf1e26b480a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"2fef3119-c9e0-421d-b77e-3cf1e26b480a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: ProspectCache - Searching DB for opportunity by owner {\"account_id\":243,\"contact_id\":4491,\"owner_id\":206} {\"correlation_id\":\"2fef3119-c9e0-421d-b77e-3cf1e26b480a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: ProspectCache - Fallback DB opportunity search {\"account_id\":243,\"contact_id\":4491} {\"correlation_id\":\"2fef3119-c9e0-421d-b77e-3cf1e26b480a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: ProspectCache - Opportunity DB search results {\"account_id\":243,\"contact_id\":4491,\"opportunity_id\":276} {\"correlation_id\":\"2fef3119-c9e0-421d-b77e-3cf1e26b480a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"2fef3119-c9e0-421d-b77e-3cf1e26b480a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612598,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"2fef3119-c9e0-421d-b77e-3cf1e26b480a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612598,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"2fef3119-c9e0-421d-b77e-3cf1e26b480a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612598} {\"correlation_id\":\"2fef3119-c9e0-421d-b77e-3cf1e26b480a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612598,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"2fef3119-c9e0-421d-b77e-3cf1e26b480a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612598,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"2fef3119-c9e0-421d-b77e-3cf1e26b480a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612597,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"f4aa8c68-1048-465c-8ab6-e414220b2b3d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612597} {\"correlation_id\":\"f4aa8c68-1048-465c-8ab6-e414220b2b3d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612597,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"f4aa8c68-1048-465c-8ab6-e414220b2b3d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612597,\"participants\":[{\"id\":999855,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999856,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null}]} {\"correlation_id\":\"f4aa8c68-1048-465c-8ab6-e414220b2b3d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"f4aa8c68-1048-465c-8ab6-e414220b2b3d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"f4aa8c68-1048-465c-8ab6-e414220b2b3d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"f4aa8c68-1048-465c-8ab6-e414220b2b3d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"f4aa8c68-1048-465c-8ab6-e414220b2b3d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612597,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"f4aa8c68-1048-465c-8ab6-e414220b2b3d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"f4aa8c68-1048-465c-8ab6-e414220b2b3d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612597,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"f4aa8c68-1048-465c-8ab6-e414220b2b3d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612597} {\"correlation_id\":\"f4aa8c68-1048-465c-8ab6-e414220b2b3d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612597,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"f4aa8c68-1048-465c-8ab6-e414220b2b3d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612597,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"f4aa8c68-1048-465c-8ab6-e414220b2b3d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612596,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"885d60db-2622-4f69-9826-b15464181dcb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612596} {\"correlation_id\":\"885d60db-2622-4f69-9826-b15464181dcb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612596,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"885d60db-2622-4f69-9826-b15464181dcb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612596,\"participants\":[{\"id\":999853,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999854,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null}]} {\"correlation_id\":\"885d60db-2622-4f69-9826-b15464181dcb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"885d60db-2622-4f69-9826-b15464181dcb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"885d60db-2622-4f69-9826-b15464181dcb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"885d60db-2622-4f69-9826-b15464181dcb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"885d60db-2622-4f69-9826-b15464181dcb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612596,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"885d60db-2622-4f69-9826-b15464181dcb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"885d60db-2622-4f69-9826-b15464181dcb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612596,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"885d60db-2622-4f69-9826-b15464181dcb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612596} {\"correlation_id\":\"885d60db-2622-4f69-9826-b15464181dcb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612596,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"885d60db-2622-4f69-9826-b15464181dcb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612596,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"885d60db-2622-4f69-9826-b15464181dcb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612595,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"0bc8a5e9-778b-4d99-bf0c-d903bd129c55\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612595} {\"correlation_id\":\"0bc8a5e9-778b-4d99-bf0c-d903bd129c55\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612595,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"0bc8a5e9-778b-4d99-bf0c-d903bd129c55\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612595,\"participants\":[{\"id\":999851,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null},{\"id\":999852,\"user_id\":206,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"0bc8a5e9-778b-4d99-bf0c-d903bd129c55\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"0bc8a5e9-778b-4d99-bf0c-d903bd129c55\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"0bc8a5e9-778b-4d99-bf0c-d903bd129c55\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"0bc8a5e9-778b-4d99-bf0c-d903bd129c55\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"0bc8a5e9-778b-4d99-bf0c-d903bd129c55\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"0bc8a5e9-778b-4d99-bf0c-d903bd129c55\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612595,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"0bc8a5e9-778b-4d99-bf0c-d903bd129c55\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612595,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"0bc8a5e9-778b-4d99-bf0c-d903bd129c55\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612595} {\"correlation_id\":\"0bc8a5e9-778b-4d99-bf0c-d903bd129c55\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612595,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"0bc8a5e9-778b-4d99-bf0c-d903bd129c55\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612595,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"0bc8a5e9-778b-4d99-bf0c-d903bd129c55\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612594,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"9710b4b5-e8f1-4fbf-b865-162ea340046c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612594} {\"correlation_id\":\"9710b4b5-e8f1-4fbf-b865-162ea340046c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612594,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"9710b4b5-e8f1-4fbf-b865-162ea340046c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612594,\"participants\":[{\"id\":999849,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999850,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null}]} {\"correlation_id\":\"9710b4b5-e8f1-4fbf-b865-162ea340046c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"9710b4b5-e8f1-4fbf-b865-162ea340046c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"9710b4b5-e8f1-4fbf-b865-162ea340046c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"9710b4b5-e8f1-4fbf-b865-162ea340046c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"9710b4b5-e8f1-4fbf-b865-162ea340046c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612594,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"9710b4b5-e8f1-4fbf-b865-162ea340046c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"9710b4b5-e8f1-4fbf-b865-162ea340046c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612594,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"9710b4b5-e8f1-4fbf-b865-162ea340046c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612594} {\"correlation_id\":\"9710b4b5-e8f1-4fbf-b865-162ea340046c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612594,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"9710b4b5-e8f1-4fbf-b865-162ea340046c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612594,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"9710b4b5-e8f1-4fbf-b865-162ea340046c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612593,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"59a8137b-0518-4c91-b63d-c794df57a8c1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612593} {\"correlation_id\":\"59a8137b-0518-4c91-b63d-c794df57a8c1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612593,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"59a8137b-0518-4c91-b63d-c794df57a8c1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612593,\"participants\":[{\"id\":999847,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999848,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null}]} {\"correlation_id\":\"59a8137b-0518-4c91-b63d-c794df57a8c1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"59a8137b-0518-4c91-b63d-c794df57a8c1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"59a8137b-0518-4c91-b63d-c794df57a8c1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"59a8137b-0518-4c91-b63d-c794df57a8c1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"59a8137b-0518-4c91-b63d-c794df57a8c1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612593,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"59a8137b-0518-4c91-b63d-c794df57a8c1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"59a8137b-0518-4c91-b63d-c794df57a8c1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612593,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"59a8137b-0518-4c91-b63d-c794df57a8c1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612593} {\"correlation_id\":\"59a8137b-0518-4c91-b63d-c794df57a8c1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612593,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"59a8137b-0518-4c91-b63d-c794df57a8c1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612593,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"59a8137b-0518-4c91-b63d-c794df57a8c1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612592,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"e1f2c9b0-a985-4277-b0da-5190fb9b25d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612592} {\"correlation_id\":\"e1f2c9b0-a985-4277-b0da-5190fb9b25d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612592,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"e1f2c9b0-a985-4277-b0da-5190fb9b25d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612592,\"participants\":[{\"id\":999845,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null},{\"id\":999846,\"user_id\":206,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"e1f2c9b0-a985-4277-b0da-5190fb9b25d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"e1f2c9b0-a985-4277-b0da-5190fb9b25d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"e1f2c9b0-a985-4277-b0da-5190fb9b25d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"e1f2c9b0-a985-4277-b0da-5190fb9b25d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"e1f2c9b0-a985-4277-b0da-5190fb9b25d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"e1f2c9b0-a985-4277-b0da-5190fb9b25d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612592,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"e1f2c9b0-a985-4277-b0da-5190fb9b25d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612592,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"e1f2c9b0-a985-4277-b0da-5190fb9b25d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612592} {\"correlation_id\":\"e1f2c9b0-a985-4277-b0da-5190fb9b25d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612592,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"e1f2c9b0-a985-4277-b0da-5190fb9b25d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612592,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"e1f2c9b0-a985-4277-b0da-5190fb9b25d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612591,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"8a5f33ea-490a-41e3-b71a-872c52626583\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612591} {\"correlation_id\":\"8a5f33ea-490a-41e3-b71a-872c52626583\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612591,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"8a5f33ea-490a-41e3-b71a-872c52626583\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612591,\"participants\":[{\"id\":999843,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999844,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null}]} {\"correlation_id\":\"8a5f33ea-490a-41e3-b71a-872c52626583\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"8a5f33ea-490a-41e3-b71a-872c52626583\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"8a5f33ea-490a-41e3-b71a-872c52626583\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"8a5f33ea-490a-41e3-b71a-872c52626583\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"8a5f33ea-490a-41e3-b71a-872c52626583\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612591,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"8a5f33ea-490a-41e3-b71a-872c52626583\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"8a5f33ea-490a-41e3-b71a-872c52626583\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612591,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"8a5f33ea-490a-41e3-b71a-872c52626583\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612591} {\"correlation_id\":\"8a5f33ea-490a-41e3-b71a-872c52626583\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612591,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"8a5f33ea-490a-41e3-b71a-872c52626583\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612591,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"8a5f33ea-490a-41e3-b71a-872c52626583\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612590,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"b34fb574-2229-4ff9-b572-7e2f801295a8\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612590} {\"correlation_id\":\"b34fb574-2229-4ff9-b572-7e2f801295a8\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612590,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"b34fb574-2229-4ff9-b572-7e2f801295a8\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612590,\"participants\":[{\"id\":999841,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999842,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null}]} {\"correlation_id\":\"b34fb574-2229-4ff9-b572-7e2f801295a8\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"b34fb574-2229-4ff9-b572-7e2f801295a8\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"b34fb574-2229-4ff9-b572-7e2f801295a8\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"b34fb574-2229-4ff9-b572-7e2f801295a8\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"b34fb574-2229-4ff9-b572-7e2f801295a8\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612590,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"b34fb574-2229-4ff9-b572-7e2f801295a8\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"b34fb574-2229-4ff9-b572-7e2f801295a8\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612590,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"b34fb574-2229-4ff9-b572-7e2f801295a8\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612590} {\"correlation_id\":\"b34fb574-2229-4ff9-b572-7e2f801295a8\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612590,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"b34fb574-2229-4ff9-b572-7e2f801295a8\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612590,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"b34fb574-2229-4ff9-b572-7e2f801295a8\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612589,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"746b5162-98d0-4e35-96ca-f6756e9b2d39\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612589} {\"correlation_id\":\"746b5162-98d0-4e35-96ca-f6756e9b2d39\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612589,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"746b5162-98d0-4e35-96ca-f6756e9b2d39\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612589,\"participants\":[{\"id\":999839,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999840,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null}]} {\"correlation_id\":\"746b5162-98d0-4e35-96ca-f6756e9b2d39\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"746b5162-98d0-4e35-96ca-f6756e9b2d39\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"746b5162-98d0-4e35-96ca-f6756e9b2d39\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"746b5162-98d0-4e35-96ca-f6756e9b2d39\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"746b5162-98d0-4e35-96ca-f6756e9b2d39\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612589,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"746b5162-98d0-4e35-96ca-f6756e9b2d39\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"746b5162-98d0-4e35-96ca-f6756e9b2d39\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612589,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"746b5162-98d0-4e35-96ca-f6756e9b2d39\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612589} {\"correlation_id\":\"746b5162-98d0-4e35-96ca-f6756e9b2d39\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612589,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"746b5162-98d0-4e35-96ca-f6756e9b2d39\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612589,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"746b5162-98d0-4e35-96ca-f6756e9b2d39\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612588,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"601702ec-6d8b-4aa3-b7e2-780e758b68d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612588} {\"correlation_id\":\"601702ec-6d8b-4aa3-b7e2-780e758b68d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612588,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"601702ec-6d8b-4aa3-b7e2-780e758b68d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612588,\"participants\":[{\"id\":999837,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null},{\"id\":999838,\"user_id\":206,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"601702ec-6d8b-4aa3-b7e2-780e758b68d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"601702ec-6d8b-4aa3-b7e2-780e758b68d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"601702ec-6d8b-4aa3-b7e2-780e758b68d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"601702ec-6d8b-4aa3-b7e2-780e758b68d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"601702ec-6d8b-4aa3-b7e2-780e758b68d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"601702ec-6d8b-4aa3-b7e2-780e758b68d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612588,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"601702ec-6d8b-4aa3-b7e2-780e758b68d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612588,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"601702ec-6d8b-4aa3-b7e2-780e758b68d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612588} {\"correlation_id\":\"601702ec-6d8b-4aa3-b7e2-780e758b68d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612588,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"601702ec-6d8b-4aa3-b7e2-780e758b68d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612588,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"601702ec-6d8b-4aa3-b7e2-780e758b68d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612587,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"75f31c21-1a08-4a7a-beef-a6ee82905eb2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612587} {\"correlation_id\":\"75f31c21-1a08-4a7a-beef-a6ee82905eb2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612587,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"75f31c21-1a08-4a7a-beef-a6ee82905eb2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612587,\"participants\":[{\"id\":999835,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999836,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null}]} {\"correlation_id\":\"75f31c21-1a08-4a7a-beef-a6ee82905eb2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"75f31c21-1a08-4a7a-beef-a6ee82905eb2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"75f31c21-1a08-4a7a-beef-a6ee82905eb2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"75f31c21-1a08-4a7a-beef-a6ee82905eb2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"75f31c21-1a08-4a7a-beef-a6ee82905eb2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612587,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"75f31c21-1a08-4a7a-beef-a6ee82905eb2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"75f31c21-1a08-4a7a-beef-a6ee82905eb2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612587,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"75f31c21-1a08-4a7a-beef-a6ee82905eb2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612587} {\"correlation_id\":\"75f31c21-1a08-4a7a-beef-a6ee82905eb2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612587,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"75f31c21-1a08-4a7a-beef-a6ee82905eb2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612587,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"75f31c21-1a08-4a7a-beef-a6ee82905eb2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612586,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"b79deb62-f26f-4831-8192-2fca40cc0445\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612586} {\"correlation_id\":\"b79deb62-f26f-4831-8192-2fca40cc0445\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612586,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"b79deb62-f26f-4831-8192-2fca40cc0445\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612586,\"participants\":[{\"id\":999833,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null},{\"id\":999834,\"user_id\":206,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"b79deb62-f26f-4831-8192-2fca40cc0445\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"b79deb62-f26f-4831-8192-2fca40cc0445\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"b79deb62-f26f-4831-8192-2fca40cc0445\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"b79deb62-f26f-4831-8192-2fca40cc0445\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"b79deb62-f26f-4831-8192-2fca40cc0445\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"b79deb62-f26f-4831-8192-2fca40cc0445\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612586,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"b79deb62-f26f-4831-8192-2fca40cc0445\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612586,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"b79deb62-f26f-4831-8192-2fca40cc0445\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612586} {\"correlation_id\":\"b79deb62-f26f-4831-8192-2fca40cc0445\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612586,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"b79deb62-f26f-4831-8192-2fca40cc0445\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612586,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"b79deb62-f26f-4831-8192-2fca40cc0445\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612585,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"2aa37843-11f3-4f11-a9ef-3277ddd89ec3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612585} {\"correlation_id\":\"2aa37843-11f3-4f11-a9ef-3277ddd89ec3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612585,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"2aa37843-11f3-4f11-a9ef-3277ddd89ec3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612585,\"participants\":[{\"id\":999831,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999832,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null}]} {\"correlation_id\":\"2aa37843-11f3-4f11-a9ef-3277ddd89ec3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"2aa37843-11f3-4f11-a9ef-3277ddd89ec3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"2aa37843-11f3-4f11-a9ef-3277ddd89ec3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"2aa37843-11f3-4f11-a9ef-3277ddd89ec3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"2aa37843-11f3-4f11-a9ef-3277ddd89ec3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612585,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"2aa37843-11f3-4f11-a9ef-3277ddd89ec3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"2aa37843-11f3-4f11-a9ef-3277ddd89ec3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612585,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"2aa37843-11f3-4f11-a9ef-3277ddd89ec3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612585} {\"correlation_id\":\"2aa37843-11f3-4f11-a9ef-3277ddd89ec3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612585,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"2aa37843-11f3-4f11-a9ef-3277ddd89ec3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612585,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"2aa37843-11f3-4f11-a9ef-3277ddd89ec3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612584,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"07bfbf03-ba7c-47ad-8252-88913ec31644\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612584} {\"correlation_id\":\"07bfbf03-ba7c-47ad-8252-88913ec31644\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612584,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"07bfbf03-ba7c-47ad-8252-88913ec31644\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612584,\"participants\":[{\"id\":999829,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999830,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null}]} {\"correlation_id\":\"07bfbf03-ba7c-47ad-8252-88913ec31644\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"07bfbf03-ba7c-47ad-8252-88913ec31644\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"07bfbf03-ba7c-47ad-8252-88913ec31644\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"07bfbf03-ba7c-47ad-8252-88913ec31644\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"07bfbf03-ba7c-47ad-8252-88913ec31644\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612584,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"07bfbf03-ba7c-47ad-8252-88913ec31644\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"07bfbf03-ba7c-47ad-8252-88913ec31644\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612584,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"07bfbf03-ba7c-47ad-8252-88913ec31644\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612584} {\"correlation_id\":\"07bfbf03-ba7c-47ad-8252-88913ec31644\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612584,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"07bfbf03-ba7c-47ad-8252-88913ec31644\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612584,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"07bfbf03-ba7c-47ad-8252-88913ec31644\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612583,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"64e9fc7f-c17e-4af3-b884-54a2c388e8f0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612583} {\"correlation_id\":\"64e9fc7f-c17e-4af3-b884-54a2c388e8f0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612583,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"64e9fc7f-c17e-4af3-b884-54a2c388e8f0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612583,\"participants\":[{\"id\":999827,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null},{\"id\":999828,\"user_id\":206,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"64e9fc7f-c17e-4af3-b884-54a2c388e8f0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"64e9fc7f-c17e-4af3-b884-54a2c388e8f0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"64e9fc7f-c17e-4af3-b884-54a2c388e8f0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"64e9fc7f-c17e-4af3-b884-54a2c388e8f0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"64e9fc7f-c17e-4af3-b884-54a2c388e8f0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"64e9fc7f-c17e-4af3-b884-54a2c388e8f0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612583,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"64e9fc7f-c17e-4af3-b884-54a2c388e8f0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612583,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"64e9fc7f-c17e-4af3-b884-54a2c388e8f0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612583} {\"correlation_id\":\"64e9fc7f-c17e-4af3-b884-54a2c388e8f0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612583,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"64e9fc7f-c17e-4af3-b884-54a2c388e8f0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612583,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"64e9fc7f-c17e-4af3-b884-54a2c388e8f0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612582,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"23188089-fcae-491d-b35d-c20491564cfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612582} {\"correlation_id\":\"23188089-fcae-491d-b35d-c20491564cfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612582,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"23188089-fcae-491d-b35d-c20491564cfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612582,\"participants\":[{\"id\":999825,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999826,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null}]} {\"correlation_id\":\"23188089-fcae-491d-b35d-c20491564cfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"23188089-fcae-491d-b35d-c20491564cfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"23188089-fcae-491d-b35d-c20491564cfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"23188089-fcae-491d-b35d-c20491564cfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"23188089-fcae-491d-b35d-c20491564cfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612582,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"23188089-fcae-491d-b35d-c20491564cfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"23188089-fcae-491d-b35d-c20491564cfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612582,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"23188089-fcae-491d-b35d-c20491564cfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612582} {\"correlation_id\":\"23188089-fcae-491d-b35d-c20491564cfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612582,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"23188089-fcae-491d-b35d-c20491564cfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612582,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"23188089-fcae-491d-b35d-c20491564cfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"9f9cc209-aee5-4d58-b4ad-1455693e83d6\",\"trace_id\":\"f11e2691-d834-4325-91f7-003e08888b34\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612581,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"91305be5-0942-40ad-86c8-c2b2f018ea2b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612581} {\"correlation_id\":\"91305be5-0942-40ad-86c8-c2b2f018ea2b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612581,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"91305be5-0942-40ad-86c8-c2b2f018ea2b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612581,\"participants\":[{\"id\":999823,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null},{\"id\":999824,\"user_id\":206,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"91305be5-0942-40ad-86c8-c2b2f018ea2b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"91305be5-0942-40ad-86c8-c2b2f018ea2b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"91305be5-0942-40ad-86c8-c2b2f018ea2b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"91305be5-0942-40ad-86c8-c2b2f018ea2b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"91305be5-0942-40ad-86c8-c2b2f018ea2b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"91305be5-0942-40ad-86c8-c2b2f018ea2b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612581,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"91305be5-0942-40ad-86c8-c2b2f018ea2b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612581,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"91305be5-0942-40ad-86c8-c2b2f018ea2b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612581} {\"correlation_id\":\"91305be5-0942-40ad-86c8-c2b2f018ea2b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612581,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"91305be5-0942-40ad-86c8-c2b2f018ea2b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [EmailSchedule] STARTING batch process {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"9f9cc209-aee5-4d58-b4ad-1455693e83d6\",\"trace_id\":\"f11e2691-d834-4325-91f7-003e08888b34\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612581,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"91305be5-0942-40ad-86c8-c2b2f018ea2b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612565,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"be6ed1a4-4495-4ec9-932f-da6698c31cec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [EmailSchedule] FINISHED batch process {\"host\":\"docker_lamp_1\",\"processed\":0} {\"correlation_id\":\"9f9cc209-aee5-4d58-b4ad-1455693e83d6\",\"trace_id\":\"f11e2691-d834-4325-91f7-003e08888b34\"}\n[2026-05-11 10:43:29] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":60.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"9f9cc209-aee5-4d58-b4ad-1455693e83d6\",\"trace_id\":\"f11e2691-d834-4325-91f7-003e08888b34\"}\n[2026-05-11 10:43:29] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612565} {\"correlation_id\":\"be6ed1a4-4495-4ec9-932f-da6698c31cec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612565,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"be6ed1a4-4495-4ec9-932f-da6698c31cec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612565,\"participants\":[{\"id\":999789,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999790,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null}]} {\"correlation_id\":\"be6ed1a4-4495-4ec9-932f-da6698c31cec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"be6ed1a4-4495-4ec9-932f-da6698c31cec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"be6ed1a4-4495-4ec9-932f-da6698c31cec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"be6ed1a4-4495-4ec9-932f-da6698c31cec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"be6ed1a4-4495-4ec9-932f-da6698c31cec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612565,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"be6ed1a4-4495-4ec9-932f-da6698c31cec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"be6ed1a4-4495-4ec9-932f-da6698c31cec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612565,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"be6ed1a4-4495-4ec9-932f-da6698c31cec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612565} {\"correlation_id\":\"be6ed1a4-4495-4ec9-932f-da6698c31cec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612565,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"be6ed1a4-4495-4ec9-932f-da6698c31cec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612565,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"be6ed1a4-4495-4ec9-932f-da6698c31cec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612563,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":350,\"stage_id\":34}} {\"correlation_id\":\"77b39c9c-1e21-4c1d-925b-4acce1b204d3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612563} {\"correlation_id\":\"77b39c9c-1e21-4c1d-925b-4acce1b204d3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612563,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"77b39c9c-1e21-4c1d-925b-4acce1b204d3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612563,\"participants\":[{\"id\":999784,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999785,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null}]} {\"correlation_id\":\"77b39c9c-1e21-4c1d-925b-4acce1b204d3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"77b39c9c-1e21-4c1d-925b-4acce1b204d3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"77b39c9c-1e21-4c1d-925b-4acce1b204d3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"77b39c9c-1e21-4c1d-925b-4acce1b204d3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"77b39c9c-1e21-4c1d-925b-4acce1b204d3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612563,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"77b39c9c-1e21-4c1d-925b-4acce1b204d3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: ProspectCache - Searching DB for opportunity by owner {\"account_id\":244,\"contact_id\":4487,\"owner_id\":206} {\"correlation_id\":\"77b39c9c-1e21-4c1d-925b-4acce1b204d3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: ProspectCache - Fallback DB opportunity search {\"account_id\":244,\"contact_id\":4487} {\"correlation_id\":\"77b39c9c-1e21-4c1d-925b-4acce1b204d3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: ProspectCache - Opportunity DB search results {\"account_id\":244,\"contact_id\":4487,\"opportunity_id\":350} {\"correlation_id\":\"77b39c9c-1e21-4c1d-925b-4acce1b204d3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"77b39c9c-1e21-4c1d-925b-4acce1b204d3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:30] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612563,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"77b39c9c-1e21-4c1d-925b-4acce1b204d3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:30] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612563} {\"correlation_id\":\"77b39c9c-1e21-4c1d-925b-4acce1b204d3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:30] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612563,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"77b39c9c-1e21-4c1d-925b-4acce1b204d3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:30] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612563,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":350,\"stage_id\":34} {\"correlation_id\":\"77b39c9c-1e21-4c1d-925b-4acce1b204d3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:30] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612562,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"485e0be1-a971-4cde-9ead-4276768bdf66\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:30] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612562} {\"correlation_id\":\"485e0be1-a971-4cde-9ead-4276768bdf66\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:30] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612562,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"485e0be1-a971-4cde-9ead-4276768bdf66\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:30] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612562,\"participants\":[{\"id\":999782,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":999783,\"user_id\":206,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"485e0be1-a971-4cde-9ead-4276768bdf66\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:30] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"485e0be1-a971-4cde-9ead-4276768bdf66\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:30] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"485e0be1-a971-4cde-9ead-4276768bdf66\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:30] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"485e0be1-a971-4cde-9ead-4276768bdf66\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:30] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"485e0be1-a971-4cde-9ead-4276768bdf66\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:30] local.INFO: [Prospect match] Cache miss, calling the API {\"identifier_type\":\"email\",\"identifier\":\"447782589921@txt.staging.jiminny.com\"} {\"correlation_id\":\"485e0be1-a971-4cde-9ead-4276768bdf66\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:30] local.INFO: [Hubspot] Failed to fetch contact {\"email\":\"447782589921@txt.staging.jiminny.com\",\"reason\":\"[404] Client error: `GET https://api.hubapi.com/crm/v3/objects/contacts/447782589921%40txt.staging.jiminny.com?properties=email%2Cfirstname%2Clastname%2Ccountry%2Cphone%2Cmobilephone%2Cjobtitle%2Chubspot_owner_id%2Cassociatedcompanyid%2Cphoto&archived=0&idProperty=email` resulted in a `404 Not Found` response\"} {\"correlation_id\":\"485e0be1-a971-4cde-9ead-4276768bdf66\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:30] local.INFO: [Prospect match] API returned empty result, caching the miss with empty prospect data {\"identifier_type\":\"email\",\"identifier\":\"447782589921@txt.staging.jiminny.com\"} {\"correlation_id\":\"485e0be1-a971-4cde-9ead-4276768bdf66\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:30] local.INFO: [Hubspot] Pagination completed {\"team_id\":2,\"endpoint\":\"https://api.hubapi.com/crm/v3/objects/contact/search\",\"total_requests\":1,\"total_records_fetched\":0,\"total_elapsed_seconds\":0.22,\"average_seconds_per_request\":0.22} {\"correlation_id\":\"485e0be1-a971-4cde-9ead-4276768bdf66\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:30] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"447782589921@txt.staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"485e0be1-a971-4cde-9ead-4276768bdf66\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:30] local.INFO: [Prospect match] Cache miss {\"identifier_type\":\"domain\",\"identifier\":\"jiminny.com\",\"crm\":\"hubspot\"} {\"correlation_id\":\"485e0be1-a971-4cde-9ead-4276768bdf66\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:30] local.INFO: [Prospect match] Cache miss, calling the API {\"identifier_type\":\"domain\",\"identifier\":\"jiminny.com\"} {\"correlation_id\":\"485e0be1-a971-4cde-9ead-4276768bdf66\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:31] local.INFO: [HubSpot] importAccount {\"crm_provider_id\":\"749766179\",\"config_id\":2} {\"correlation_id\":\"485e0be1-a971-4cde-9ead-4276768bdf66\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:31] local.INFO: [HubSpot] CRM Search requested {\"request\":{\"filterGroups\":[{\"filters\":[{\"propertyName\":\"associations.company\",\"operator\":\"EQ\",\"value\":\"749766179\"},{\"propertyName\":\"dealstage\",\"operator\":\"NOT_IN\",\"values\":[\"closedwon\",\"4040964\",\"59247967\"]},{\"propertyName\":\"dealstage\",\"operator\":\"NOT_IN\",\"values\":[\"closedlost\",\"4040965\",\"59247968\"]}]}],\"sorts\":[{\"propertyName\":\"modifieddate\",\"direction\":\"DESCENDING\"}],\"properties\":[\"dealname\",\"amount\",\"hubspot_owner_id\",\"pipeline\",\"dealstage\",\"closedate\",\"deal_currency_code\"],\"limit\":200}} {\"correlation_id\":\"485e0be1-a971-4cde-9ead-4276768bdf66\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:31] local.INFO: [Hubspot] Pagination completed {\"team_id\":2,\"endpoint\":\"https://api.hubapi.com/crm/v3/objects/deals/search\",\"total_requests\":1,\"total_records_fetched\":10,\"total_elapsed_seconds\":0.3,\"average_seconds_per_request\":0.3} {\"correlation_id\":\"485e0be1-a971-4cde-9ead-4276768bdf66\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612562,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"485e0be1-a971-4cde-9ead-4276768bdf66\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612562,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"485e0be1-a971-4cde-9ead-4276768bdf66\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612562} {\"correlation_id\":\"485e0be1-a971-4cde-9ead-4276768bdf66\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612562,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"485e0be1-a971-4cde-9ead-4276768bdf66\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612562,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"485e0be1-a971-4cde-9ead-4276768bdf66\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612561,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"9fffce69-30ed-4c66-9ee9-e980e776cd57\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612561} {\"correlation_id\":\"9fffce69-30ed-4c66-9ee9-e980e776cd57\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612561,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"9fffce69-30ed-4c66-9ee9-e980e776cd57\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612561,\"participants\":[{\"id\":999780,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999781,\"user_id\":null,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"9fffce69-30ed-4c66-9ee9-e980e776cd57\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"9fffce69-30ed-4c66-9ee9-e980e776cd57\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"9fffce69-30ed-4c66-9ee9-e980e776cd57\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"9fffce69-30ed-4c66-9ee9-e980e776cd57\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"9fffce69-30ed-4c66-9ee9-e980e776cd57\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612561,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"9fffce69-30ed-4c66-9ee9-e980e776cd57\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [Prospect match] Cache miss, calling the API {\"identifier_type\":\"email\",\"identifier\":\"447700174614.447782589921.OeREojLVnk@txt.staging.jiminny.com\"} {\"correlation_id\":\"9fffce69-30ed-4c66-9ee9-e980e776cd57\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [Hubspot] Failed to fetch contact {\"email\":\"447700174614.447782589921.OeREojLVnk@txt.staging.jiminny.com\",\"reason\":\"[404] Client error: `GET https://api.hubapi.com/crm/v3/objects/contacts/447700174614.447782589921.OeREojLVnk%40txt.staging.jiminny.com?properties=email%2Cfirstname%2Clastname%2Ccountry%2Cphone%2Cmobilephone%2Cjobtitle%2Chubspot_owner_id%2Cassociatedcompanyid%2Cphoto&archived=0&idProperty=email` resulted in a `404 Not Found` response\"} {\"correlation_id\":\"9fffce69-30ed-4c66-9ee9-e980e776cd57\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [Prospect match] API returned empty result, caching the miss with empty prospect data {\"identifier_type\":\"email\",\"identifier\":\"447700174614.447782589921.OeREojLVnk@txt.staging.jiminny.com\"} {\"correlation_id\":\"9fffce69-30ed-4c66-9ee9-e980e776cd57\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"447700174614.447782589921.OeREojLVnk@txt.staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"9fffce69-30ed-4c66-9ee9-e980e776cd57\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612561,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"9fffce69-30ed-4c66-9ee9-e980e776cd57\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612561} {\"correlation_id\":\"9fffce69-30ed-4c66-9ee9-e980e776cd57\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612561,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"9fffce69-30ed-4c66-9ee9-e980e776cd57\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612561,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"9fffce69-30ed-4c66-9ee9-e980e776cd57\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612560,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"6a946453-4ffe-4df1-b91d-f73baf850a7a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612560} {\"correlation_id\":\"6a946453-4ffe-4df1-b91d-f73baf850a7a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612560,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"6a946453-4ffe-4df1-b91d-f73baf850a7a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612560,\"participants\":[{\"id\":999778,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":999779,\"user_id\":206,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"6a946453-4ffe-4df1-b91d-f73baf850a7a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"6a946453-4ffe-4df1-b91d-f73baf850a7a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"6a946453-4ffe-4df1-b91d-f73baf850a7a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"6a946453-4ffe-4df1-b91d-f73baf850a7a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"6a946453-4ffe-4df1-b91d-f73baf850a7a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"447782589921@txt.staging.jiminny.com\"} {\"correlation_id\":\"6a946453-4ffe-4df1-b91d-f73baf850a7a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"447782589921@txt.staging.jiminny.com\"} {\"correlation_id\":\"6a946453-4ffe-4df1-b91d-f73baf850a7a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"447782589921@txt.staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"6a946453-4ffe-4df1-b91d-f73baf850a7a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612560,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"6a946453-4ffe-4df1-b91d-f73baf850a7a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612560,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"6a946453-4ffe-4df1-b91d-f73baf850a7a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612560} {\"correlation_id\":\"6a946453-4ffe-4df1-b91d-f73baf850a7a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612560,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"6a946453-4ffe-4df1-b91d-f73baf850a7a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612560,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"6a946453-4ffe-4df1-b91d-f73baf850a7a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612559,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":5011,\"stage_id\":34}} {\"correlation_id\":\"6231af67-0852-4f81-a268-953b6db0b3aa\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612559} {\"correlation_id\":\"6231af67-0852-4f81-a268-953b6db0b3aa\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612559,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"6231af67-0852-4f81-a268-953b6db0b3aa\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612559,\"participants\":[{\"id\":999776,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999777,\"user_id\":null,\"contact_id\":97,\"lead_id\":null}]} {\"correlation_id\":\"6231af67-0852-4f81-a268-953b6db0b3aa\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"6231af67-0852-4f81-a268-953b6db0b3aa\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"6231af67-0852-4f81-a268-953b6db0b3aa\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"6231af67-0852-4f81-a268-953b6db0b3aa\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"6231af67-0852-4f81-a268-953b6db0b3aa\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612559,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"6231af67-0852-4f81-a268-953b6db0b3aa\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: ProspectCache - Searching DB for opportunity by owner {\"account_id\":69,\"contact_id\":97,\"owner_id\":206} {\"correlation_id\":\"6231af67-0852-4f81-a268-953b6db0b3aa\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: ProspectCache - Fallback DB opportunity search {\"account_id\":69,\"contact_id\":97} {\"correlation_id\":\"6231af67-0852-4f81-a268-953b6db0b3aa\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: ProspectCache - Opportunity DB search results {\"account_id\":69,\"contact_id\":97,\"opportunity_id\":5011} {\"correlation_id\":\"6231af67-0852-4f81-a268-953b6db0b3aa\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinsoncrusoe@test.com\"} {\"correlation_id\":\"6231af67-0852-4f81-a268-953b6db0b3aa\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612559,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"6231af67-0852-4f81-a268-953b6db0b3aa\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612559} {\"correlation_id\":\"6231af67-0852-4f81-a268-953b6db0b3aa\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612559,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"6231af67-0852-4f81-a268-953b6db0b3aa\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612559,\"remote_search\":true,\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":5011,\"stage_id\":34} {\"correlation_id\":\"6231af67-0852-4f81-a268-953b6db0b3aa\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612558,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":5011,\"stage_id\":34}} {\"correlation_id\":\"f8c4783d-18f2-45b6-a1b8-e9d80b788104\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612558} {\"correlation_id\":\"f8c4783d-18f2-45b6-a1b8-e9d80b788104\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612558,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"f8c4783d-18f2-45b6-a1b8-e9d80b788104\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612558,\"participants\":[{\"id\":999774,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999775,\"user_id\":null,\"contact_id\":97,\"lead_id\":null}]} {\"correlation_id\":\"f8c4783d-18f2-45b6-a1b8-e9d80b788104\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"f8c4783d-18f2-45b6-a1b8-e9d80b788104\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"f8c4783d-18f2-45b6-a1b8-e9d80b788104\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"f8c4783d-18f2-45b6-a1b8-e9d80b788104\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"f8c4783d-18f2-45b6-a1b8-e9d80b788104\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612558,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"f8c4783d-18f2-45b6-a1b8-e9d80b788104\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinsoncrusoe@test.com\"} {\"correlation_id\":\"f8c4783d-18f2-45b6-a1b8-e9d80b788104\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612558,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"f8c4783d-18f2-45b6-a1b8-e9d80b788104\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612558} {\"correlation_id\":\"f8c4783d-18f2-45b6-a1b8-e9d80b788104\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612558,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"f8c4783d-18f2-45b6-a1b8-e9d80b788104\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612558,\"remote_search\":true,\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":5011,\"stage_id\":34} {\"correlation_id\":\"f8c4783d-18f2-45b6-a1b8-e9d80b788104\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612557,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":5011,\"stage_id\":34}} {\"correlation_id\":\"d087a635-67a6-4fb4-be19-85f309031ebb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612557} {\"correlation_id\":\"d087a635-67a6-4fb4-be19-85f309031ebb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612557,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"d087a635-67a6-4fb4-be19-85f309031ebb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612557,\"participants\":[{\"id\":999772,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999773,\"user_id\":null,\"contact_id\":97,\"lead_id\":null}]} {\"correlation_id\":\"d087a635-67a6-4fb4-be19-85f309031ebb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"d087a635-67a6-4fb4-be19-85f309031ebb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"d087a635-67a6-4fb4-be19-85f309031ebb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d087a635-67a6-4fb4-be19-85f309031ebb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"d087a635-67a6-4fb4-be19-85f309031ebb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612557,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"d087a635-67a6-4fb4-be19-85f309031ebb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinsoncrusoe@test.com\"} {\"correlation_id\":\"d087a635-67a6-4fb4-be19-85f309031ebb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612557,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"d087a635-67a6-4fb4-be19-85f309031ebb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612557} {\"correlation_id\":\"d087a635-67a6-4fb4-be19-85f309031ebb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612557,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"d087a635-67a6-4fb4-be19-85f309031ebb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612557,\"remote_search\":true,\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":5011,\"stage_id\":34} {\"correlation_id\":\"d087a635-67a6-4fb4-be19-85f309031ebb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612556,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":5011,\"stage_id\":34}} {\"correlation_id\":\"462bb4b0-f644-4f69-b4a8-6030370c2202\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612556} {\"correlation_id\":\"462bb4b0-f644-4f69-b4a8-6030370c2202\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612556,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"462bb4b0-f644-4f69-b4a8-6030370c2202\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612556,\"participants\":[{\"id\":999770,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999771,\"user_id\":null,\"contact_id\":97,\"lead_id\":null}]} {\"correlation_id\":\"462bb4b0-f644-4f69-b4a8-6030370c2202\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"462bb4b0-f644-4f69-b4a8-6030370c2202\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"462bb4b0-f644-4f69-b4a8-6030370c2202\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"462bb4b0-f644-4f69-b4a8-6030370c2202\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"462bb4b0-f644-4f69-b4a8-6030370c2202\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612556,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"462bb4b0-f644-4f69-b4a8-6030370c2202\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinsoncrusoe@test.com\"} {\"correlation_id\":\"462bb4b0-f644-4f69-b4a8-6030370c2202\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612556,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"462bb4b0-f644-4f69-b4a8-6030370c2202\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612556} {\"correlation_id\":\"462bb4b0-f644-4f69-b4a8-6030370c2202\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612556,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"462bb4b0-f644-4f69-b4a8-6030370c2202\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612556,\"remote_search\":true,\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":5011,\"stage_id\":34} {\"correlation_id\":\"462bb4b0-f644-4f69-b4a8-6030370c2202\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612555,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":5011,\"stage_id\":34}} {\"correlation_id\":\"988eea33-83d6-41f7-93a3-9737c43ee6a3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612555} {\"correlation_id\":\"988eea33-83d6-41f7-93a3-9737c43ee6a3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [ EsUpdateProcessManager ] Finished updating entities in ES {\"worker\":\"\",\"peak_memory\":\"99.73 MB\",\"elapsed_seconds\":4.12,\"update_target\":\"activities\",\"should_iterate_again\":false} {\"correlation_id\":\"4ad05333-9afb-492e-9f0f-b2909ac45b32\",\"trace_id\":\"3d8feb24-b173-4158-b0a4-4cf33af85066\"}\n[2026-05-11 10:43:33] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612555,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"988eea33-83d6-41f7-93a3-9737c43ee6a3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612555,\"participants\":[{\"id\":999768,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999769,\"user_id\":null,\"contact_id\":97,\"lead_id\":null}]} {\"correlation_id\":\"988eea33-83d6-41f7-93a3-9737c43ee6a3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"988eea33-83d6-41f7-93a3-9737c43ee6a3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"988eea33-83d6-41f7-93a3-9737c43ee6a3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"988eea33-83d6-41f7-93a3-9737c43ee6a3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"988eea33-83d6-41f7-93a3-9737c43ee6a3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612555,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"988eea33-83d6-41f7-93a3-9737c43ee6a3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinsoncrusoe@test.com\"} {\"correlation_id\":\"988eea33-83d6-41f7-93a3-9737c43ee6a3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612555,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"988eea33-83d6-41f7-93a3-9737c43ee6a3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612555} {\"correlation_id\":\"988eea33-83d6-41f7-93a3-9737c43ee6a3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612555,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"988eea33-83d6-41f7-93a3-9737c43ee6a3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612555,\"remote_search\":true,\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":5011,\"stage_id\":34} {\"correlation_id\":\"988eea33-83d6-41f7-93a3-9737c43ee6a3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612554,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":5011,\"stage_id\":34}} {\"correlation_id\":\"7ff43be6-a32a-412c-bb3d-1bd40aa30f65\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612554} {\"correlation_id\":\"7ff43be6-a32a-412c-bb3d-1bd40aa30f65\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612554,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"7ff43be6-a32a-412c-bb3d-1bd40aa30f65\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612554,\"participants\":[{\"id\":999766,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999767,\"user_id\":null,\"contact_id\":97,\"lead_id\":null}]} {\"correlation_id\":\"7ff43be6-a32a-412c-bb3d-1bd40aa30f65\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"7ff43be6-a32a-412c-bb3d-1bd40aa30f65\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"7ff43be6-a32a-412c-bb3d-1bd40aa30f65\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"7ff43be6-a32a-412c-bb3d-1bd40aa30f65\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"7ff43be6-a32a-412c-bb3d-1bd40aa30f65\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612554,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"7ff43be6-a32a-412c-bb3d-1bd40aa30f65\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinsoncrusoe@test.com\"} {\"correlation_id\":\"7ff43be6-a32a-412c-bb3d-1bd40aa30f65\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612554,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"7ff43be6-a32a-412c-bb3d-1bd40aa30f65\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612554} {\"correlation_id\":\"7ff43be6-a32a-412c-bb3d-1bd40aa30f65\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612554,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"7ff43be6-a32a-412c-bb3d-1bd40aa30f65\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612554,\"remote_search\":true,\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":5011,\"stage_id\":34} {\"correlation_id\":\"7ff43be6-a32a-412c-bb3d-1bd40aa30f65\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612553,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":5011,\"stage_id\":34}} {\"correlation_id\":\"3e6b8bf5-c432-4502-a026-aaf235b3b359\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612553} {\"correlation_id\":\"3e6b8bf5-c432-4502-a026-aaf235b3b359\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612553,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"3e6b8bf5-c432-4502-a026-aaf235b3b359\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612553,\"participants\":[{\"id\":999764,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999765,\"user_id\":null,\"contact_id\":97,\"lead_id\":null}]} {\"correlation_id\":\"3e6b8bf5-c432-4502-a026-aaf235b3b359\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"3e6b8bf5-c432-4502-a026-aaf235b3b359\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"3e6b8bf5-c432-4502-a026-aaf235b3b359\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"3e6b8bf5-c432-4502-a026-aaf235b3b359\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"3e6b8bf5-c432-4502-a026-aaf235b3b359\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612553,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"3e6b8bf5-c432-4502-a026-aaf235b3b359\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinsoncrusoe@test.com\"} {\"correlation_id\":\"3e6b8bf5-c432-4502-a026-aaf235b3b359\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612553,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"3e6b8bf5-c432-4502-a026-aaf235b3b359\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612553} {\"correlation_id\":\"3e6b8bf5-c432-4502-a026-aaf235b3b359\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612553,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"3e6b8bf5-c432-4502-a026-aaf235b3b359\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612553,\"remote_search\":true,\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":5011,\"stage_id\":34} {\"correlation_id\":\"3e6b8bf5-c432-4502-a026-aaf235b3b359\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"calendar:sync\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:34] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612552,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":5011,\"stage_id\":34}} {\"correlation_id\":\"1d70d062-f785-4c2c-81be-d992f1ef25c0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612552} {\"correlation_id\":\"1d70d062-f785-4c2c-81be-d992f1ef25c0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:retry-failed\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"c2dd1ea7-a2bd-44ee-95a4-60f88c2fcef7\",\"trace_id\":\"dcda0937-682f-46f0-bdc7-d2054de63c89\"}\n[2026-05-11 10:43:34] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612552,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"1d70d062-f785-4c2c-81be-d992f1ef25c0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612552,\"participants\":[{\"id\":999762,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999763,\"user_id\":null,\"contact_id\":97,\"lead_id\":null}]} {\"correlation_id\":\"1d70d062-f785-4c2c-81be-d992f1ef25c0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.NOTICE: Calendar sync start {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:34] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"1d70d062-f785-4c2c-81be-d992f1ef25c0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"1d70d062-f785-4c2c-81be-d992f1ef25c0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"1d70d062-f785-4c2c-81be-d992f1ef25c0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"1d70d062-f785-4c2c-81be-d992f1ef25c0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612552,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"1d70d062-f785-4c2c-81be-d992f1ef25c0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinsoncrusoe@test.com\"} {\"correlation_id\":\"1d70d062-f785-4c2c-81be-d992f1ef25c0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612552,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"1d70d062-f785-4c2c-81be-d992f1ef25c0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612552} {\"correlation_id\":\"1d70d062-f785-4c2c-81be-d992f1ef25c0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612552,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"1d70d062-f785-4c2c-81be-d992f1ef25c0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612552,\"remote_search\":true,\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":5011,\"stage_id\":34} {\"correlation_id\":\"1d70d062-f785-4c2c-81be-d992f1ef25c0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:retry-failed\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":60.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"c2dd1ea7-a2bd-44ee-95a4-60f88c2fcef7\",\"trace_id\":\"dcda0937-682f-46f0-bdc7-d2054de63c89\"}\n[2026-05-11 10:43:34] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612551,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":5011,\"stage_id\":34}} {\"correlation_id\":\"35f5694e-2521-4089-a5f1-4adc747a40bf\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612551} {\"correlation_id\":\"35f5694e-2521-4089-a5f1-4adc747a40bf\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612551,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"35f5694e-2521-4089-a5f1-4adc747a40bf\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612551,\"participants\":[{\"id\":999760,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999761,\"user_id\":null,\"contact_id\":97,\"lead_id\":null}]} {\"correlation_id\":\"35f5694e-2521-4089-a5f1-4adc747a40bf\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"35f5694e-2521-4089-a5f1-4adc747a40bf\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"35f5694e-2521-4089-a5f1-4adc747a40bf\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"35f5694e-2521-4089-a5f1-4adc747a40bf\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"35f5694e-2521-4089-a5f1-4adc747a40bf\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612551,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"35f5694e-2521-4089-a5f1-4adc747a40bf\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinsoncrusoe@test.com\"} {\"correlation_id\":\"35f5694e-2521-4089-a5f1-4adc747a40bf\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612551,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"35f5694e-2521-4089-a5f1-4adc747a40bf\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612551} {\"correlation_id\":\"35f5694e-2521-4089-a5f1-4adc747a40bf\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612551,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"35f5694e-2521-4089-a5f1-4adc747a40bf\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612551,\"remote_search\":true,\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":5011,\"stage_id\":34} {\"correlation_id\":\"35f5694e-2521-4089-a5f1-4adc747a40bf\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1393,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:34] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1393,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:34] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612550,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":5011,\"stage_id\":34}} {\"correlation_id\":\"046f384b-a938-4138-a9e6-7df0ff6eb90b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:34] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612550} {\"correlation_id\":\"046f384b-a938-4138-a9e6-7df0ff6eb90b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612550,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"046f384b-a938-4138-a9e6-7df0ff6eb90b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1393,\"provider\":\"google\",\"refreshToken\":\"5aa7e2d96b53201cd16fca5d2e4ef3ad03320971fc064781d18aee3ae7b99fbf\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:34] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612550,\"participants\":[{\"id\":999758,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999759,\"user_id\":null,\"contact_id\":97,\"lead_id\":null}]} {\"correlation_id\":\"046f384b-a938-4138-a9e6-7df0ff6eb90b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"046f384b-a938-4138-a9e6-7df0ff6eb90b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"046f384b-a938-4138-a9e6-7df0ff6eb90b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"046f384b-a938-4138-a9e6-7df0ff6eb90b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"046f384b-a938-4138-a9e6-7df0ff6eb90b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612550,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"046f384b-a938-4138-a9e6-7df0ff6eb90b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinsoncrusoe@test.com\"} {\"correlation_id\":\"046f384b-a938-4138-a9e6-7df0ff6eb90b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612550,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"046f384b-a938-4138-a9e6-7df0ff6eb90b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612550} {\"correlation_id\":\"046f384b-a938-4138-a9e6-7df0ff6eb90b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612550,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"046f384b-a938-4138-a9e6-7df0ff6eb90b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612550,\"remote_search\":true,\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":5011,\"stage_id\":34} {\"correlation_id\":\"046f384b-a938-4138-a9e6-7df0ff6eb90b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612549,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":5011,\"stage_id\":34}} {\"correlation_id\":\"1fe4380c-c6ba-40c4-9bdc-65cf0a157cda\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612549} {\"correlation_id\":\"1fe4380c-c6ba-40c4-9bdc-65cf0a157cda\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612549,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"1fe4380c-c6ba-40c4-9bdc-65cf0a157cda\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612549,\"participants\":[{\"id\":999756,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999757,\"user_id\":null,\"contact_id\":97,\"lead_id\":null}]} {\"correlation_id\":\"1fe4380c-c6ba-40c4-9bdc-65cf0a157cda\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1393,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Account has been deleted\"}} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:34] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:34] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1393,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:34] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"1fe4380c-c6ba-40c4-9bdc-65cf0a157cda\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"1fe4380c-c6ba-40c4-9bdc-65cf0a157cda\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"1fe4380c-c6ba-40c4-9bdc-65cf0a157cda\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"1fe4380c-c6ba-40c4-9bdc-65cf0a157cda\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1387,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:34] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1387,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:34] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:34] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612549,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"1fe4380c-c6ba-40c4-9bdc-65cf0a157cda\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1387,\"provider\":\"google\",\"refreshToken\":\"8157ac6de94842937194009e9c50e459253600f799dacf6a40755ffdbeb5bba6\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:34] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinsoncrusoe@test.com\"} {\"correlation_id\":\"1fe4380c-c6ba-40c4-9bdc-65cf0a157cda\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612549,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"1fe4380c-c6ba-40c4-9bdc-65cf0a157cda\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612549} {\"correlation_id\":\"1fe4380c-c6ba-40c4-9bdc-65cf0a157cda\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612549,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"1fe4380c-c6ba-40c4-9bdc-65cf0a157cda\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612549,\"remote_search\":true,\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":5011,\"stage_id\":34} {\"correlation_id\":\"1fe4380c-c6ba-40c4-9bdc-65cf0a157cda\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612365,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"9f080e5f-9e25-468c-9d04-5cdf618dc528\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612365} {\"correlation_id\":\"9f080e5f-9e25-468c-9d04-5cdf618dc528\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612365,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"9f080e5f-9e25-468c-9d04-5cdf618dc528\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612365,\"participants\":[{\"id\":999563,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null},{\"id\":999564,\"user_id\":206,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"9f080e5f-9e25-468c-9d04-5cdf618dc528\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"9f080e5f-9e25-468c-9d04-5cdf618dc528\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"9f080e5f-9e25-468c-9d04-5cdf618dc528\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"9f080e5f-9e25-468c-9d04-5cdf618dc528\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1387,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Account has been deleted\"}} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"9f080e5f-9e25-468c-9d04-5cdf618dc528\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1387,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"9f080e5f-9e25-468c-9d04-5cdf618dc528\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612365,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"9f080e5f-9e25-468c-9d04-5cdf618dc528\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1348,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612365,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"9f080e5f-9e25-468c-9d04-5cdf618dc528\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1348,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612365} {\"correlation_id\":\"9f080e5f-9e25-468c-9d04-5cdf618dc528\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612365,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"9f080e5f-9e25-468c-9d04-5cdf618dc528\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1348,\"provider\":\"google\",\"refreshToken\":\"9e7d13d3032d0cb1b79d8e95aef01383e8e91eb52ff8ee960c8a0b6b95cd8c73\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612365,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"9f080e5f-9e25-468c-9d04-5cdf618dc528\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612360,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"e1af9830-ccba-44db-a1a0-d9ac3ec0da4d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612360} {\"correlation_id\":\"e1af9830-ccba-44db-a1a0-d9ac3ec0da4d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612360,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"e1af9830-ccba-44db-a1a0-d9ac3ec0da4d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612360,\"participants\":[{\"id\":999552,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null},{\"id\":999553,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999565,\"user_id\":null,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"e1af9830-ccba-44db-a1a0-d9ac3ec0da4d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"e1af9830-ccba-44db-a1a0-d9ac3ec0da4d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"e1af9830-ccba-44db-a1a0-d9ac3ec0da4d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"e1af9830-ccba-44db-a1a0-d9ac3ec0da4d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"e1af9830-ccba-44db-a1a0-d9ac3ec0da4d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"e1af9830-ccba-44db-a1a0-d9ac3ec0da4d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612360,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"e1af9830-ccba-44db-a1a0-d9ac3ec0da4d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1348,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Bad Request\"}} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1348,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1361,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1361,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1361,\"provider\":\"google\",\"refreshToken\":\"6c843da199c2b9907445329304fcc4ec5057a4ee748d8299641764395c08e1fd\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1361,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Account has been deleted\"}} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1361,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [Hubspot] Pagination completed {\"team_id\":2,\"endpoint\":\"https://api.hubapi.com/crm/v3/objects/contact/search\",\"total_requests\":1,\"total_records_fetched\":0,\"total_elapsed_seconds\":0.24,\"average_seconds_per_request\":0.24} {\"correlation_id\":\"e1af9830-ccba-44db-a1a0-d9ac3ec0da4d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612360,\"participants_processed\":3,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"e1af9830-ccba-44db-a1a0-d9ac3ec0da4d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1310,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612360} {\"correlation_id\":\"e1af9830-ccba-44db-a1a0-d9ac3ec0da4d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1310,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612360,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"e1af9830-ccba-44db-a1a0-d9ac3ec0da4d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1310,\"provider\":\"google\",\"refreshToken\":\"e34818922c2830a660813a63f6169a4a9a992ae2cccd7dc8dd7796cfdb470ef1\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612360,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"e1af9830-ccba-44db-a1a0-d9ac3ec0da4d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612340,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"eb3baeb0-9f99-4e3d-98f1-fdc0d69023fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612340} {\"correlation_id\":\"eb3baeb0-9f99-4e3d-98f1-fdc0d69023fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612340,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"eb3baeb0-9f99-4e3d-98f1-fdc0d69023fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612340,\"participants\":[{\"id\":999516,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null},{\"id\":999517,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999518,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":999519,\"user_id\":null,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"eb3baeb0-9f99-4e3d-98f1-fdc0d69023fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"eb3baeb0-9f99-4e3d-98f1-fdc0d69023fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"eb3baeb0-9f99-4e3d-98f1-fdc0d69023fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"eb3baeb0-9f99-4e3d-98f1-fdc0d69023fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"eb3baeb0-9f99-4e3d-98f1-fdc0d69023fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"eb3baeb0-9f99-4e3d-98f1-fdc0d69023fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612340,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"eb3baeb0-9f99-4e3d-98f1-fdc0d69023fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1310,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Bad Request\"}} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1310,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1333,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1333,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1333,\"provider\":\"google\",\"refreshToken\":\"6c902986546d8e8da1dc539b046cdc1d458f519acc972e5b5f1d6a1a295165e0\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [Hubspot] Pagination completed {\"team_id\":2,\"endpoint\":\"https://api.hubapi.com/crm/v3/objects/contact/search\",\"total_requests\":1,\"total_records_fetched\":0,\"total_elapsed_seconds\":0.22,\"average_seconds_per_request\":0.22} {\"correlation_id\":\"eb3baeb0-9f99-4e3d-98f1-fdc0d69023fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612340,\"participants_processed\":4,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"eb3baeb0-9f99-4e3d-98f1-fdc0d69023fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612340} {\"correlation_id\":\"eb3baeb0-9f99-4e3d-98f1-fdc0d69023fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612340,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"eb3baeb0-9f99-4e3d-98f1-fdc0d69023fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612340,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"eb3baeb0-9f99-4e3d-98f1-fdc0d69023fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612339,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"68ce2e50-cf66-4aab-9ce0-ab242e6027c4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612339} {\"correlation_id\":\"68ce2e50-cf66-4aab-9ce0-ab242e6027c4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612339,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"68ce2e50-cf66-4aab-9ce0-ab242e6027c4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612339,\"participants\":[{\"id\":999514,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null},{\"id\":999515,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999540,\"user_id\":null,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"68ce2e50-cf66-4aab-9ce0-ab242e6027c4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"68ce2e50-cf66-4aab-9ce0-ab242e6027c4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"68ce2e50-cf66-4aab-9ce0-ab242e6027c4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"68ce2e50-cf66-4aab-9ce0-ab242e6027c4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"68ce2e50-cf66-4aab-9ce0-ab242e6027c4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"68ce2e50-cf66-4aab-9ce0-ab242e6027c4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612339,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"68ce2e50-cf66-4aab-9ce0-ab242e6027c4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612339,\"participants_processed\":3,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"68ce2e50-cf66-4aab-9ce0-ab242e6027c4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1333,\"provider\":\"google\",\"responseBody\":{\"error\":\"unauthorized_client\",\"error_description\":\"Unauthorized\"}} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612339} {\"correlation_id\":\"68ce2e50-cf66-4aab-9ce0-ab242e6027c4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1333,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612339,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"68ce2e50-cf66-4aab-9ce0-ab242e6027c4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612339,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"68ce2e50-cf66-4aab-9ce0-ab242e6027c4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1368,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1368,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1368,\"provider\":\"google\",\"refreshToken\":\"d2f128898ff8543bd16b69cfae37896ab85119b0f5ed2b431d739593bb600333\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612336,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"ffc0ea4a-744d-49a4-a9ad-736fbda03384\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612336} {\"correlation_id\":\"ffc0ea4a-744d-49a4-a9ad-736fbda03384\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612336,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"ffc0ea4a-744d-49a4-a9ad-736fbda03384\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612336,\"participants\":[{\"id\":999508,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null},{\"id\":999509,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999512,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":999513,\"user_id\":null,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"ffc0ea4a-744d-49a4-a9ad-736fbda03384\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"ffc0ea4a-744d-49a4-a9ad-736fbda03384\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"ffc0ea4a-744d-49a4-a9ad-736fbda03384\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"ffc0ea4a-744d-49a4-a9ad-736fbda03384\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"ffc0ea4a-744d-49a4-a9ad-736fbda03384\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"ffc0ea4a-744d-49a4-a9ad-736fbda03384\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612336,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"ffc0ea4a-744d-49a4-a9ad-736fbda03384\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612336,\"participants_processed\":4,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"ffc0ea4a-744d-49a4-a9ad-736fbda03384\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612336} {\"correlation_id\":\"ffc0ea4a-744d-49a4-a9ad-736fbda03384\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612336,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"ffc0ea4a-744d-49a4-a9ad-736fbda03384\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612336,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"ffc0ea4a-744d-49a4-a9ad-736fbda03384\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612183,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36}} {\"correlation_id\":\"48880be8-e032-41b2-948e-c4ec99247548\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612183} {\"correlation_id\":\"48880be8-e032-41b2-948e-c4ec99247548\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612183,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"48880be8-e032-41b2-948e-c4ec99247548\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612183,\"participants\":[{\"id\":999227,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null},{\"id\":999228,\"user_id\":261,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"48880be8-e032-41b2-948e-c4ec99247548\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"48880be8-e032-41b2-948e-c4ec99247548\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"48880be8-e032-41b2-948e-c4ec99247548\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"48880be8-e032-41b2-948e-c4ec99247548\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"48880be8-e032-41b2-948e-c4ec99247548\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"48880be8-e032-41b2-948e-c4ec99247548\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612183,\"team_id\":2,\"email\":\"adelina.petrova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"48880be8-e032-41b2-948e-c4ec99247548\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612183,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"48880be8-e032-41b2-948e-c4ec99247548\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612183} {\"correlation_id\":\"48880be8-e032-41b2-948e-c4ec99247548\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612183,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"48880be8-e032-41b2-948e-c4ec99247548\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612183,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36} {\"correlation_id\":\"48880be8-e032-41b2-948e-c4ec99247548\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612182,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36}} {\"correlation_id\":\"3858f13d-426c-474e-98cf-11576b98e5dd\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612182} {\"correlation_id\":\"3858f13d-426c-474e-98cf-11576b98e5dd\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612182,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"3858f13d-426c-474e-98cf-11576b98e5dd\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612182,\"participants\":[{\"id\":999225,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null},{\"id\":999226,\"user_id\":261,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"3858f13d-426c-474e-98cf-11576b98e5dd\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"3858f13d-426c-474e-98cf-11576b98e5dd\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"3858f13d-426c-474e-98cf-11576b98e5dd\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"3858f13d-426c-474e-98cf-11576b98e5dd\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"3858f13d-426c-474e-98cf-11576b98e5dd\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"3858f13d-426c-474e-98cf-11576b98e5dd\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612182,\"team_id\":2,\"email\":\"adelina.petrova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"3858f13d-426c-474e-98cf-11576b98e5dd\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612182,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"3858f13d-426c-474e-98cf-11576b98e5dd\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612182} {\"correlation_id\":\"3858f13d-426c-474e-98cf-11576b98e5dd\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612182,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"3858f13d-426c-474e-98cf-11576b98e5dd\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612182,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36} {\"correlation_id\":\"3858f13d-426c-474e-98cf-11576b98e5dd\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612181,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36}} {\"correlation_id\":\"33fd8677-950a-4069-9642-511743b8b619\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612181} {\"correlation_id\":\"33fd8677-950a-4069-9642-511743b8b619\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612181,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"33fd8677-950a-4069-9642-511743b8b619\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612181,\"participants\":[{\"id\":999223,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null},{\"id\":999224,\"user_id\":261,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"33fd8677-950a-4069-9642-511743b8b619\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"33fd8677-950a-4069-9642-511743b8b619\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"33fd8677-950a-4069-9642-511743b8b619\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"33fd8677-950a-4069-9642-511743b8b619\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"33fd8677-950a-4069-9642-511743b8b619\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"33fd8677-950a-4069-9642-511743b8b619\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1368,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Bad Request\"}} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612181,\"team_id\":2,\"email\":\"adelina.petrova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"33fd8677-950a-4069-9642-511743b8b619\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1368,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612181,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"33fd8677-950a-4069-9642-511743b8b619\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612181} {\"correlation_id\":\"33fd8677-950a-4069-9642-511743b8b619\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612181,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"33fd8677-950a-4069-9642-511743b8b619\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612181,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36} {\"correlation_id\":\"33fd8677-950a-4069-9642-511743b8b619\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1365,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1365,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1365,\"provider\":\"google\",\"refreshToken\":\"7676e4a9afcd082b413248ab5ec6e487021fec6a9bdf315860a59cefad9caad8\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612180,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36}} {\"correlation_id\":\"dc184f1d-d7e2-4bc1-92a0-c572af69ba86\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612180} {\"correlation_id\":\"dc184f1d-d7e2-4bc1-92a0-c572af69ba86\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612180,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"dc184f1d-d7e2-4bc1-92a0-c572af69ba86\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612180,\"participants\":[{\"id\":999221,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null},{\"id\":999222,\"user_id\":261,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"dc184f1d-d7e2-4bc1-92a0-c572af69ba86\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"dc184f1d-d7e2-4bc1-92a0-c572af69ba86\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"dc184f1d-d7e2-4bc1-92a0-c572af69ba86\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"dc184f1d-d7e2-4bc1-92a0-c572af69ba86\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"dc184f1d-d7e2-4bc1-92a0-c572af69ba86\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"dc184f1d-d7e2-4bc1-92a0-c572af69ba86\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612180,\"team_id\":2,\"email\":\"adelina.petrova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"dc184f1d-d7e2-4bc1-92a0-c572af69ba86\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612180,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"dc184f1d-d7e2-4bc1-92a0-c572af69ba86\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612180} {\"correlation_id\":\"dc184f1d-d7e2-4bc1-92a0-c572af69ba86\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612180,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"dc184f1d-d7e2-4bc1-92a0-c572af69ba86\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612180,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36} {\"correlation_id\":\"dc184f1d-d7e2-4bc1-92a0-c572af69ba86\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":611455,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"8f14fe33-00cc-4910-996e-efa541fc5bb7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":611455} {\"correlation_id\":\"8f14fe33-00cc-4910-996e-efa541fc5bb7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":611455,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"8f14fe33-00cc-4910-996e-efa541fc5bb7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":611455,\"participants\":[{\"id\":997961,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":997962,\"user_id\":1460,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"8f14fe33-00cc-4910-996e-efa541fc5bb7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"8f14fe33-00cc-4910-996e-efa541fc5bb7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"8f14fe33-00cc-4910-996e-efa541fc5bb7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"8f14fe33-00cc-4910-996e-efa541fc5bb7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"8f14fe33-00cc-4910-996e-efa541fc5bb7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [Prospect match] Cache miss, calling the API {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"8f14fe33-00cc-4910-996e-efa541fc5bb7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1365,\"provider\":\"google\",\"responseBody\":{\"error\":\"unauthorized_client\",\"error_description\":\"Unauthorized\"}} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1365,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1364,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1364,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1364,\"provider\":\"google\",\"refreshToken\":\"dd5882ebce76e645292ce33ae74238abbb77c0a4ecc6a2bfe723cad82e72ba8e\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.INFO: [Hubspot] Failed to fetch contact {\"email\":\"support@staging.jiminny.com\",\"reason\":\"[404] Client error: `GET https://api.hubapi.com/crm/v3/objects/contacts/support%40staging.jiminny.com?properties=email%2Cfirstname%2Clastname%2Ccountry%2Cphone%2Cmobilephone%2Cjobtitle%2Chubspot_owner_id%2Cassociatedcompanyid%2Cphoto&archived=0&idProperty=email` resulted in a `404 Not Found` response\"} {\"correlation_id\":\"8f14fe33-00cc-4910-996e-efa541fc5bb7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [Prospect match] API returned empty result, caching the miss with empty prospect data {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"8f14fe33-00cc-4910-996e-efa541fc5bb7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1364,\"provider\":\"google\",\"responseBody\":{\"error\":\"unauthorized_client\",\"error_description\":\"Unauthorized\"}} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1364,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1370,\"provider\":\"office\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1370,\"provider\":\"office\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1370,\"provider\":\"office\",\"refreshToken\":\"b7ee8035306d0043cea6e00e7c4fe14f745e44074a1194db62a31cdf8b70af3e\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.INFO: [Hubspot] Pagination completed {\"team_id\":2,\"endpoint\":\"https://api.hubapi.com/crm/v3/objects/contact/search\",\"total_requests\":1,\"total_records_fetched\":0,\"total_elapsed_seconds\":0.21,\"average_seconds_per_request\":0.21} {\"correlation_id\":\"8f14fe33-00cc-4910-996e-efa541fc5bb7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"8f14fe33-00cc-4910-996e-efa541fc5bb7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [Prospect match] Cache miss {\"identifier_type\":\"domain\",\"identifier\":\"jiminny.com\",\"crm\":\"hubspot\"} {\"correlation_id\":\"8f14fe33-00cc-4910-996e-efa541fc5bb7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [Prospect match] Cache miss, calling the API {\"identifier_type\":\"domain\",\"identifier\":\"jiminny.com\"} {\"correlation_id\":\"8f14fe33-00cc-4910-996e-efa541fc5bb7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1370,\"provider\":\"office\",\"responseBody\":\"{\\\"error\\\":\\\"invalid_client\\\",\\\"error_description\\\":\\\"AADSTS7000215: Invalid client secret provided. Ensure the secret being sent in the request is the client secret value, not the client secret ID, for a secret added to app 'bbcbb2ef-6200-4fae-82bd-d81f5dd738da'. Trace ID: 329b429c-bad8-4f19-bace-40e48a111900 Correlation ID: 4f12c9af-e118-468c-8441-ebc325071d4b Timestamp: 2026-05-11 10:43:36Z\\\",\\\"error_codes\\\":[7000215],\\\"timestamp\\\":\\\"2026-05-11 10:43:36Z\\\",\\\"trace_id\\\":\\\"329b429c-bad8-4f19-bace-40e48a111900\\\",\\\"correlation_id\\\":\\\"4f12c9af-e118-468c-8441-ebc325071d4b\\\",\\\"error_uri\\\":\\\"https://login.microsoftonline.com/error?code=7000215\\\"}\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1370,\"provider\":\"office\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1202,\"provider\":\"office\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1202,\"provider\":\"office\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1202,\"provider\":\"office\",\"refreshToken\":\"b458799ccc29b21a6e2eb5260fdb63e49ccba21bf942a3973fb63799bd7f0afe\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.INFO: [HubSpot] importAccount {\"crm_provider_id\":\"749766179\",\"config_id\":2} {\"correlation_id\":\"8f14fe33-00cc-4910-996e-efa541fc5bb7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [HubSpot] CRM Search requested {\"request\":{\"filterGroups\":[{\"filters\":[{\"propertyName\":\"associations.company\",\"operator\":\"EQ\",\"value\":\"749766179\"},{\"propertyName\":\"dealstage\",\"operator\":\"NOT_IN\",\"values\":[\"closedwon\",\"4040964\",\"59247967\"]},{\"propertyName\":\"dealstage\",\"operator\":\"NOT_IN\",\"values\":[\"closedlost\",\"4040965\",\"59247968\"]}]}],\"sorts\":[{\"propertyName\":\"modifieddate\",\"direction\":\"DESCENDING\"}],\"properties\":[\"dealname\",\"amount\",\"hubspot_owner_id\",\"pipeline\",\"dealstage\",\"closedate\",\"deal_currency_code\"],\"limit\":200}} {\"correlation_id\":\"8f14fe33-00cc-4910-996e-efa541fc5bb7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:37] local.INFO: [Hubspot] Pagination completed {\"team_id\":2,\"endpoint\":\"https://api.hubapi.com/crm/v3/objects/deals/search\",\"total_requests\":1,\"total_records_fetched\":10,\"total_elapsed_seconds\":0.24,\"average_seconds_per_request\":0.24} {\"correlation_id\":\"8f14fe33-00cc-4910-996e-efa541fc5bb7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:37] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":611455,\"team_id\":2,\"email\":\"aneliya.angelova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"8f14fe33-00cc-4910-996e-efa541fc5bb7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:37] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":611455,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"8f14fe33-00cc-4910-996e-efa541fc5bb7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:37] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":611455} {\"correlation_id\":\"8f14fe33-00cc-4910-996e-efa541fc5bb7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:37] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":611455,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"8f14fe33-00cc-4910-996e-efa541fc5bb7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:37] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":611455,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"8f14fe33-00cc-4910-996e-efa541fc5bb7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:37] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":611451,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"7ea49a4a-ec21-4b93-b990-d1e79531b72a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:37] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":611451} {\"correlation_id\":\"7ea49a4a-ec21-4b93-b990-d1e79531b72a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:37] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":611451,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"7ea49a4a-ec21-4b93-b990-d1e79531b72a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:37] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":611451,\"participants\":[{\"id\":997955,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":997956,\"user_id\":18,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"7ea49a4a-ec21-4b93-b990-d1e79531b72a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:37] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"7ea49a4a-ec21-4b93-b990-d1e79531b72a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:37] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"7ea49a4a-ec21-4b93-b990-d1e79531b72a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:37] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"7ea49a4a-ec21-4b93-b990-d1e79531b72a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:37] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"7ea49a4a-ec21-4b93-b990-d1e79531b72a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:37] local.INFO: [Prospect match] Cache miss, calling the API {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"7ea49a4a-ec21-4b93-b990-d1e79531b72a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:38] local.INFO: [Hubspot] Failed to fetch contact {\"email\":\"support@staging.jiminny.com\",\"reason\":\"[404] Client error: `GET https://api.hubapi.com/crm/v3/objects/contacts/support%40staging.jiminny.com?properties=email%2Cfirstname%2Clastname%2Ccountry%2Cphone%2Cmobilephone%2Cjobtitle%2Chubspot_owner_id%2Cassociatedcompanyid%2Cphoto&archived=0&idProperty=email` resulted in a `404 Not Found` response\"} {\"correlation_id\":\"7ea49a4a-ec21-4b93-b990-d1e79531b72a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:38] local.INFO: [Prospect match] API returned empty result, caching the miss with empty prospect data {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"7ea49a4a-ec21-4b93-b990-d1e79531b72a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:38] local.INFO: [Hubspot] Pagination completed {\"team_id\":2,\"endpoint\":\"https://api.hubapi.com/crm/v3/objects/contact/search\",\"total_requests\":1,\"total_records_fetched\":0,\"total_elapsed_seconds\":0.22,\"average_seconds_per_request\":0.22} {\"correlation_id\":\"7ea49a4a-ec21-4b93-b990-d1e79531b72a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:38] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"7ea49a4a-ec21-4b93-b990-d1e79531b72a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:38] local.INFO: [Prospect match] Cache miss {\"identifier_type\":\"domain\",\"identifier\":\"jiminny.com\",\"crm\":\"hubspot\"} {\"correlation_id\":\"7ea49a4a-ec21-4b93-b990-d1e79531b72a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:38] local.INFO: [Prospect match] Cache miss, calling the API {\"identifier_type\":\"domain\",\"identifier\":\"jiminny.com\"} {\"correlation_id\":\"7ea49a4a-ec21-4b93-b990-d1e79531b72a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:38] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1202,\"provider\":\"office\",\"responseBody\":\"{\\\"error\\\":\\\"invalid_client\\\",\\\"error_description\\\":\\\"AADSTS7000215: Invalid client secret provided. Ensure the secret being sent in the request is the client secret value, not the client secret ID, for a secret added to app 'bbcbb2ef-6200-4fae-82bd-d81f5dd738da'. Trace ID: e8a4c6ea-4ab7-4915-b7a3-93868da51100 Correlation ID: e670afc0-d33c-4205-924c-76bda5c3d01d Timestamp: 2026-05-11 10:43:38Z\\\",\\\"error_codes\\\":[7000215],\\\"timestamp\\\":\\\"2026-05-11 10:43:38Z\\\",\\\"trace_id\\\":\\\"e8a4c6ea-4ab7-4915-b7a3-93868da51100\\\",\\\"correlation_id\\\":\\\"e670afc0-d33c-4205-924c-76bda5c3d01d\\\",\\\"error_uri\\\":\\\"https://login.microsoftonline.com/error?code=7000215\\\"}\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:38] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:38] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1202,\"provider\":\"office\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:38] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1502,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:38] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1502,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:38] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:38] local.INFO: Calendar sync job dispatched {\"calendar_id\":501} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:38] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1300,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:38] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1300,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:38] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:38] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1300,\"provider\":\"google\",\"refreshToken\":\"4b811db0725fd9602a95943519a7da935e2a5065da7d9ebfcb170752e3e1ddb8\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:38] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1300,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Account has been deleted\"}} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:38] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:38] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1300,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:38] local.INFO: [HubSpot] importAccount {\"crm_provider_id\":\"749766179\",\"config_id\":2} {\"correlation_id\":\"7ea49a4a-ec21-4b93-b990-d1e79531b72a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:38] local.INFO: [HubSpot] CRM Search requested {\"request\":{\"filterGroups\":[{\"filters\":[{\"propertyName\":\"associations.company\",\"operator\":\"EQ\",\"value\":\"749766179\"},{\"propertyName\":\"dealstage\",\"operator\":\"NOT_IN\",\"values\":[\"closedwon\",\"4040964\",\"59247967\"]},{\"propertyName\":\"dealstage\",\"operator\":\"NOT_IN\",\"values\":[\"closedlost\",\"4040965\",\"59247968\"]}]}],\"sorts\":[{\"propertyName\":\"modifieddate\",\"direction\":\"DESCENDING\"}],\"properties\":[\"dealname\",\"amount\",\"hubspot_owner_id\",\"pipeline\",\"dealstage\",\"closedate\",\"deal_currency_code\"],\"limit\":200}} {\"correlation_id\":\"7ea49a4a-ec21-4b93-b990-d1e79531b72a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:38] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1409,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:38] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1409,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:38] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:38] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1409,\"provider\":\"google\",\"refreshToken\":\"e2a3f2d06894894eed1ee87d9db1ace77d4d42ee6e1288a8940ad2c10333b0c4\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:38] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1409,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Bad Request\"}} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:38] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:38] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1409,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:39] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1352,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:39] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1352,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:39] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:39] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1352,\"provider\":\"google\",\"refreshToken\":\"dd4b16b00fdc1216da6b717c02338c073636e29162826b2de6db3f064fc029eb\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:39] local.INFO: [Hubspot] Pagination completed {\"team_id\":2,\"endpoint\":\"https://api.hubapi.com/crm/v3/objects/deals/search\",\"total_requests\":1,\"total_records_fetched\":10,\"total_elapsed_seconds\":0.28,\"average_seconds_per_request\":0.28} {\"correlation_id\":\"7ea49a4a-ec21-4b93-b990-d1e79531b72a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1352,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Token has been expired or revoked.\"}} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:39] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:39] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1352,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:39] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1296,\"provider\":\"office\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:39] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1296,\"provider\":\"office\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:39] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:39] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1296,\"provider\":\"office\",\"refreshToken\":\"011ae723c9d800c674e0b4be76f49fc046dac7d501b66c59ef0d9549cfa56ae5\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:39] local.INFO: [ EsUpdateProcessManager ] Finished updating entities in ES {\"worker\":\"\",\"peak_memory\":\"99.73 MB\",\"elapsed_seconds\":0.9,\"update_target\":\"activities\",\"should_iterate_again\":false} {\"correlation_id\":\"4ad05333-9afb-492e-9f0f-b2909ac45b32\",\"trace_id\":\"3d8feb24-b173-4158-b0a4-4cf33af85066\"}\n[2026-05-11 10:43:39] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":611451,\"team_id\":2,\"email\":\"veselin.kulov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"7ea49a4a-ec21-4b93-b990-d1e79531b72a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":611451,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"7ea49a4a-ec21-4b93-b990-d1e79531b72a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":611451} {\"correlation_id\":\"7ea49a4a-ec21-4b93-b990-d1e79531b72a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":611451,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"7ea49a4a-ec21-4b93-b990-d1e79531b72a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":611451,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"7ea49a4a-ec21-4b93-b990-d1e79531b72a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":611087,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"02dbf6fa-a774-4090-af49-cabec99363d4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":611087} {\"correlation_id\":\"02dbf6fa-a774-4090-af49-cabec99363d4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":611087,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"02dbf6fa-a774-4090-af49-cabec99363d4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":611087,\"participants\":[{\"id\":997368,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":997369,\"user_id\":18,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"02dbf6fa-a774-4090-af49-cabec99363d4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"02dbf6fa-a774-4090-af49-cabec99363d4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"02dbf6fa-a774-4090-af49-cabec99363d4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"02dbf6fa-a774-4090-af49-cabec99363d4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"02dbf6fa-a774-4090-af49-cabec99363d4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"02dbf6fa-a774-4090-af49-cabec99363d4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"02dbf6fa-a774-4090-af49-cabec99363d4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"02dbf6fa-a774-4090-af49-cabec99363d4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":611087,\"team_id\":2,\"email\":\"veselin.kulov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"02dbf6fa-a774-4090-af49-cabec99363d4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":611087,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"02dbf6fa-a774-4090-af49-cabec99363d4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":611087} {\"correlation_id\":\"02dbf6fa-a774-4090-af49-cabec99363d4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":611087,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"02dbf6fa-a774-4090-af49-cabec99363d4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":611087,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"02dbf6fa-a774-4090-af49-cabec99363d4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1296,\"provider\":\"office\",\"responseBody\":\"{\\\"error\\\":\\\"invalid_client\\\",\\\"error_description\\\":\\\"AADSTS7000215: Invalid client secret provided. Ensure the secret being sent in the request is the client secret value, not the client secret ID, for a secret added to app 'bbcbb2ef-6200-4fae-82bd-d81f5dd738da'. Trace ID: 4ff43bd2-d427-4049-964c-57fbcad91a00 Correlation ID: 6e92626f-a36d-4c41-ae7c-3b7296df0110 Timestamp: 2026-05-11 10:43:39Z\\\",\\\"error_codes\\\":[7000215],\\\"timestamp\\\":\\\"2026-05-11 10:43:39Z\\\",\\\"trace_id\\\":\\\"4ff43bd2-d427-4049-964c-57fbcad91a00\\\",\\\"correlation_id\\\":\\\"6e92626f-a36d-4c41-ae7c-3b7296df0110\\\",\\\"error_uri\\\":\\\"https://login.microsoftonline.com/error?code=7000215\\\"}\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:39] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:39] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1296,\"provider\":\"office\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:39] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":611076,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"ae1d456f-4b46-4c79-bb7f-bc2e95d48c9f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":611076} {\"correlation_id\":\"ae1d456f-4b46-4c79-bb7f-bc2e95d48c9f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":611076,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"ae1d456f-4b46-4c79-bb7f-bc2e95d48c9f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":611076,\"participants\":[{\"id\":997346,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":997347,\"user_id\":18,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"ae1d456f-4b46-4c79-bb7f-bc2e95d48c9f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":391,\"provider\":\"office\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:39] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":391,\"provider\":\"office\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:39] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:39] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":391,\"provider\":\"office\",\"refreshToken\":\"00045eebae0f39b34887c6d53f92ae78064f7145e1f4b67754aebd03cfb2d881\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:39] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"ae1d456f-4b46-4c79-bb7f-bc2e95d48c9f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"ae1d456f-4b46-4c79-bb7f-bc2e95d48c9f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"ae1d456f-4b46-4c79-bb7f-bc2e95d48c9f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"ae1d456f-4b46-4c79-bb7f-bc2e95d48c9f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"ae1d456f-4b46-4c79-bb7f-bc2e95d48c9f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"ae1d456f-4b46-4c79-bb7f-bc2e95d48c9f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"ae1d456f-4b46-4c79-bb7f-bc2e95d48c9f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":611076,\"team_id\":2,\"email\":\"veselin.kulov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"ae1d456f-4b46-4c79-bb7f-bc2e95d48c9f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":611076,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"ae1d456f-4b46-4c79-bb7f-bc2e95d48c9f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":611076} {\"correlation_id\":\"ae1d456f-4b46-4c79-bb7f-bc2e95d48c9f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":611076,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"ae1d456f-4b46-4c79-bb7f-bc2e95d48c9f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":611076,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"ae1d456f-4b46-4c79-bb7f-bc2e95d48c9f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1502,\"provider\":\"google\"} {\"correlation_id\":\"0fdd7dce-04e2-4696-ba87-8089a739c81c\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1502,\"provider\":\"google\"} {\"correlation_id\":\"0fdd7dce-04e2-4696-ba87-8089a739c81c\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:40] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"0fdd7dce-04e2-4696-ba87-8089a739c81c\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:40] local.INFO: [Calendar] Processing sync {\"calendarId\":\"a33076c1-8d97-431a-99f0-85c9524e118b\",\"from\":null,\"to\":null,\"delta\":\"CIiFh8TP44kDEIiFh8TP44kDGAUgkZvkzgIokZvkzgI=\",\"last_sync\":\"2024-12-09 07:12:53\",\"dateMode\":\"daily\"} {\"correlation_id\":\"0fdd7dce-04e2-4696-ba87-8089a739c81c\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:40] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"integration-app\",\"crm_owner\":1695,\"team_id\":3143} {\"correlation_id\":\"0fdd7dce-04e2-4696-ba87-8089a739c81c\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1502,\"provider\":\"google\"} {\"correlation_id\":\"0fdd7dce-04e2-4696-ba87-8089a739c81c\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1502,\"provider\":\"google\"} {\"correlation_id\":\"0fdd7dce-04e2-4696-ba87-8089a739c81c\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:40] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"0fdd7dce-04e2-4696-ba87-8089a739c81c\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":610935,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"9f5579a5-e89f-4074-9843-d5c1f123878d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610935} {\"correlation_id\":\"9f5579a5-e89f-4074-9843-d5c1f123878d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610935,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"9f5579a5-e89f-4074-9843-d5c1f123878d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":610935,\"participants\":[{\"id\":997141,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":997142,\"user_id\":18,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"9f5579a5-e89f-4074-9843-d5c1f123878d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"9f5579a5-e89f-4074-9843-d5c1f123878d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"9f5579a5-e89f-4074-9843-d5c1f123878d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"9f5579a5-e89f-4074-9843-d5c1f123878d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"9f5579a5-e89f-4074-9843-d5c1f123878d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"9f5579a5-e89f-4074-9843-d5c1f123878d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"9f5579a5-e89f-4074-9843-d5c1f123878d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"9f5579a5-e89f-4074-9843-d5c1f123878d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":610935,\"team_id\":2,\"email\":\"veselin.kulov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"9f5579a5-e89f-4074-9843-d5c1f123878d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":610935,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"9f5579a5-e89f-4074-9843-d5c1f123878d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610935} {\"correlation_id\":\"9f5579a5-e89f-4074-9843-d5c1f123878d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610935,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"9f5579a5-e89f-4074-9843-d5c1f123878d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":610935,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"9f5579a5-e89f-4074-9843-d5c1f123878d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":610915,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"ee1e1cdc-f0b6-4353-b761-ada15282a954\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610915} {\"correlation_id\":\"ee1e1cdc-f0b6-4353-b761-ada15282a954\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610915,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"ee1e1cdc-f0b6-4353-b761-ada15282a954\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":610915,\"participants\":[{\"id\":997104,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":997105,\"user_id\":18,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"ee1e1cdc-f0b6-4353-b761-ada15282a954\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"ee1e1cdc-f0b6-4353-b761-ada15282a954\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"ee1e1cdc-f0b6-4353-b761-ada15282a954\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"ee1e1cdc-f0b6-4353-b761-ada15282a954\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"ee1e1cdc-f0b6-4353-b761-ada15282a954\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"ee1e1cdc-f0b6-4353-b761-ada15282a954\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"ee1e1cdc-f0b6-4353-b761-ada15282a954\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"ee1e1cdc-f0b6-4353-b761-ada15282a954\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":610915,\"team_id\":2,\"email\":\"veselin.kulov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"ee1e1cdc-f0b6-4353-b761-ada15282a954\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":610915,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"ee1e1cdc-f0b6-4353-b761-ada15282a954\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610915} {\"correlation_id\":\"ee1e1cdc-f0b6-4353-b761-ada15282a954\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610915,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"ee1e1cdc-f0b6-4353-b761-ada15282a954\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":610915,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"ee1e1cdc-f0b6-4353-b761-ada15282a954\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":610900,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"46958b32-a07f-4aa9-a052-c6a1e3bf70b2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610900} {\"correlation_id\":\"46958b32-a07f-4aa9-a052-c6a1e3bf70b2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610900,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"46958b32-a07f-4aa9-a052-c6a1e3bf70b2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":610900,\"participants\":[{\"id\":997081,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":997082,\"user_id\":18,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"46958b32-a07f-4aa9-a052-c6a1e3bf70b2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"46958b32-a07f-4aa9-a052-c6a1e3bf70b2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"46958b32-a07f-4aa9-a052-c6a1e3bf70b2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"46958b32-a07f-4aa9-a052-c6a1e3bf70b2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"46958b32-a07f-4aa9-a052-c6a1e3bf70b2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"46958b32-a07f-4aa9-a052-c6a1e3bf70b2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"46958b32-a07f-4aa9-a052-c6a1e3bf70b2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"46958b32-a07f-4aa9-a052-c6a1e3bf70b2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":610900,\"team_id\":2,\"email\":\"veselin.kulov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"46958b32-a07f-4aa9-a052-c6a1e3bf70b2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":610900,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"46958b32-a07f-4aa9-a052-c6a1e3bf70b2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610900} {\"correlation_id\":\"46958b32-a07f-4aa9-a052-c6a1e3bf70b2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610900,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"46958b32-a07f-4aa9-a052-c6a1e3bf70b2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":610900,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"46958b32-a07f-4aa9-a052-c6a1e3bf70b2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":610885,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"b9dc6bd0-a276-4d4c-ad70-8cd444119160\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610885} {\"correlation_id\":\"b9dc6bd0-a276-4d4c-ad70-8cd444119160\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610885,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"b9dc6bd0-a276-4d4c-ad70-8cd444119160\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":610885,\"participants\":[{\"id\":997051,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":997052,\"user_id\":18,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"b9dc6bd0-a276-4d4c-ad70-8cd444119160\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"b9dc6bd0-a276-4d4c-ad70-8cd444119160\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"b9dc6bd0-a276-4d4c-ad70-8cd444119160\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"b9dc6bd0-a276-4d4c-ad70-8cd444119160\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"b9dc6bd0-a276-4d4c-ad70-8cd444119160\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"b9dc6bd0-a276-4d4c-ad70-8cd444119160\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"b9dc6bd0-a276-4d4c-ad70-8cd444119160\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"b9dc6bd0-a276-4d4c-ad70-8cd444119160\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":610885,\"team_id\":2,\"email\":\"veselin.kulov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"b9dc6bd0-a276-4d4c-ad70-8cd444119160\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":610885,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"b9dc6bd0-a276-4d4c-ad70-8cd444119160\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610885} {\"correlation_id\":\"b9dc6bd0-a276-4d4c-ad70-8cd444119160\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610885,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"b9dc6bd0-a276-4d4c-ad70-8cd444119160\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":610885,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"b9dc6bd0-a276-4d4c-ad70-8cd444119160\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":610878,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"117202f1-4432-416d-922c-d7cd0f2ae201\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610878} {\"correlation_id\":\"117202f1-4432-416d-922c-d7cd0f2ae201\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610878,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"117202f1-4432-416d-922c-d7cd0f2ae201\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":610878,\"participants\":[{\"id\":997035,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":997036,\"user_id\":18,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"117202f1-4432-416d-922c-d7cd0f2ae201\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":391,\"provider\":\"office\",\"responseBody\":\"{\\\"error\\\":\\\"invalid_client\\\",\\\"error_description\\\":\\\"AADSTS7000215: Invalid client secret provided. Ensure the secret being sent in the request is the client secret value, not the client secret ID, for a secret added to app 'bbcbb2ef-6200-4fae-82bd-d81f5dd738da'. Trace ID: 72b211a8-6a7c-4954-b720-f1a47e981b00 Correlation ID: 5f9781fe-b28a-44ce-94e3-42c683cdfd60 Timestamp: 2026-05-11 10:43:40Z\\\",\\\"error_codes\\\":[7000215],\\\"timestamp\\\":\\\"2026-05-11 10:43:40Z\\\",\\\"trace_id\\\":\\\"72b211a8-6a7c-4954-b720-f1a47e981b00\\\",\\\"correlation_id\\\":\\\"5f9781fe-b28a-44ce-94e3-42c683cdfd60\\\",\\\"error_uri\\\":\\\"https://login.microsoftonline.com/error?code=7000215\\\"}\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:40] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":391,\"provider\":\"office\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"117202f1-4432-416d-922c-d7cd0f2ae201\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"117202f1-4432-416d-922c-d7cd0f2ae201\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"117202f1-4432-416d-922c-d7cd0f2ae201\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"117202f1-4432-416d-922c-d7cd0f2ae201\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1271,\"provider\":\"office\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1271,\"provider\":\"office\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:40] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"117202f1-4432-416d-922c-d7cd0f2ae201\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1271,\"provider\":\"office\",\"refreshToken\":\"118cde2c06993147b07ccaec4cbcd5026a819dea6c71081166a492933e392afb\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"117202f1-4432-416d-922c-d7cd0f2ae201\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"117202f1-4432-416d-922c-d7cd0f2ae201\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":610878,\"team_id\":2,\"email\":\"veselin.kulov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"117202f1-4432-416d-922c-d7cd0f2ae201\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":610878,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"117202f1-4432-416d-922c-d7cd0f2ae201\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610878} {\"correlation_id\":\"117202f1-4432-416d-922c-d7cd0f2ae201\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610878,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"117202f1-4432-416d-922c-d7cd0f2ae201\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":610878,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"117202f1-4432-416d-922c-d7cd0f2ae201\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Google Calendar] Failed to watch channel for calendar {\"calendarId\":\"a33076c1-8d97-431a-99f0-85c9524e118b\",\"code\":400,\"reason\":\"{\n \\\"error\\\": {\n \\\"errors\\\": [\n {\n \\\"domain\\\": \\\"global\\\",\n \\\"reason\\\": \\\"push.webhookUrlNotHttps\\\",\n \\\"message\\\": \\\"WebHook callback must be HTTPS: /webhook/calendar/google?resourceType=event\\\"\n }\n ],\n \\\"code\\\": 400,\n \\\"message\\\": \\\"WebHook callback must be HTTPS: /webhook/calendar/google?resourceType=event\\\"\n }\n}\"} {\"correlation_id\":\"0fdd7dce-04e2-4696-ba87-8089a739c81c\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:40] local.WARNING: [Calendar] Sync failed {\"calendarId\":\"a33076c1-8d97-431a-99f0-85c9524e118b\",\"code\":400,\"reason\":\"{\n \\\"error\\\": {\n \\\"errors\\\": [\n {\n \\\"domain\\\": \\\"global\\\",\n \\\"reason\\\": \\\"push.webhookUrlNotHttps\\\",\n \\\"message\\\": \\\"WebHook callback must be HTTPS: /webhook/calendar/google?resourceType=event\\\"\n }\n ],\n \\\"code\\\": 400,\n \\\"message\\\": \\\"WebHook callback must be HTTPS: /webhook/calendar/google?resourceType=event\\\"\n }\n}\"} {\"correlation_id\":\"0fdd7dce-04e2-4696-ba87-8089a739c81c\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":610874,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"6264f69a-8461-4d85-9725-ef4c30fb5d35\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610874} {\"correlation_id\":\"6264f69a-8461-4d85-9725-ef4c30fb5d35\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610874,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"6264f69a-8461-4d85-9725-ef4c30fb5d35\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":610874,\"participants\":[{\"id\":997025,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":997026,\"user_id\":18,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"6264f69a-8461-4d85-9725-ef4c30fb5d35\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"6264f69a-8461-4d85-9725-ef4c30fb5d35\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"6264f69a-8461-4d85-9725-ef4c30fb5d35\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"6264f69a-8461-4d85-9725-ef4c30fb5d35\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"6264f69a-8461-4d85-9725-ef4c30fb5d35\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"6264f69a-8461-4d85-9725-ef4c30fb5d35\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"6264f69a-8461-4d85-9725-ef4c30fb5d35\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"6264f69a-8461-4d85-9725-ef4c30fb5d35\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":610874,\"team_id\":2,\"email\":\"veselin.kulov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"6264f69a-8461-4d85-9725-ef4c30fb5d35\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":610874,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"6264f69a-8461-4d85-9725-ef4c30fb5d35\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610874} {\"correlation_id\":\"6264f69a-8461-4d85-9725-ef4c30fb5d35\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610874,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"6264f69a-8461-4d85-9725-ef4c30fb5d35\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":610874,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"6264f69a-8461-4d85-9725-ef4c30fb5d35\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":610867,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"978698d8-e33a-4d4a-b374-04919cae6ac2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610867} {\"correlation_id\":\"978698d8-e33a-4d4a-b374-04919cae6ac2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610867,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"978698d8-e33a-4d4a-b374-04919cae6ac2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":610867,\"participants\":[{\"id\":997011,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":997012,\"user_id\":18,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"978698d8-e33a-4d4a-b374-04919cae6ac2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"978698d8-e33a-4d4a-b374-04919cae6ac2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"978698d8-e33a-4d4a-b374-04919cae6ac2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"978698d8-e33a-4d4a-b374-04919cae6ac2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"978698d8-e33a-4d4a-b374-04919cae6ac2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"978698d8-e33a-4d4a-b374-04919cae6ac2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"978698d8-e33a-4d4a-b374-04919cae6ac2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"978698d8-e33a-4d4a-b374-04919cae6ac2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":610867,\"team_id\":2,\"email\":\"veselin.kulov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"978698d8-e33a-4d4a-b374-04919cae6ac2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":610867,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"978698d8-e33a-4d4a-b374-04919cae6ac2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610867} {\"correlation_id\":\"978698d8-e33a-4d4a-b374-04919cae6ac2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610867,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"978698d8-e33a-4d4a-b374-04919cae6ac2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":610867,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"978698d8-e33a-4d4a-b374-04919cae6ac2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":610764,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"3e294e41-3a19-4afe-87b3-4f7f0336107c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610764} {\"correlation_id\":\"3e294e41-3a19-4afe-87b3-4f7f0336107c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610764,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"3e294e41-3a19-4afe-87b3-4f7f0336107c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":610764,\"participants\":[{\"id\":996951,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":996952,\"user_id\":18,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"3e294e41-3a19-4afe-87b3-4f7f0336107c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"3e294e41-3a19-4afe-87b3-4f7f0336107c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"3e294e41-3a19-4afe-87b3-4f7f0336107c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"3e294e41-3a19-4afe-87b3-4f7f0336107c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"3e294e41-3a19-4afe-87b3-4f7f0336107c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"3e294e41-3a19-4afe-87b3-4f7f0336107c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"3e294e41-3a19-4afe-87b3-4f7f0336107c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"3e294e41-3a19-4afe-87b3-4f7f0336107c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":610764,\"team_id\":2,\"email\":\"veselin.kulov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"3e294e41-3a19-4afe-87b3-4f7f0336107c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":610764,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"3e294e41-3a19-4afe-87b3-4f7f0336107c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610764} {\"correlation_id\":\"3e294e41-3a19-4afe-87b3-4f7f0336107c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610764,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"3e294e41-3a19-4afe-87b3-4f7f0336107c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":610764,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"3e294e41-3a19-4afe-87b3-4f7f0336107c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":610617,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"732b2bbb-ee7f-4bb2-85b1-e6478955544b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610617} {\"correlation_id\":\"732b2bbb-ee7f-4bb2-85b1-e6478955544b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610617,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"732b2bbb-ee7f-4bb2-85b1-e6478955544b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":610617,\"participants\":[{\"id\":996641,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":996642,\"user_id\":18,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"732b2bbb-ee7f-4bb2-85b1-e6478955544b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"732b2bbb-ee7f-4bb2-85b1-e6478955544b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"732b2bbb-ee7f-4bb2-85b1-e6478955544b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"732b2bbb-ee7f-4bb2-85b1-e6478955544b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"732b2bbb-ee7f-4bb2-85b1-e6478955544b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"732b2bbb-ee7f-4bb2-85b1-e6478955544b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"732b2bbb-ee7f-4bb2-85b1-e6478955544b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"732b2bbb-ee7f-4bb2-85b1-e6478955544b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":610617,\"team_id\":2,\"email\":\"veselin.kulov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"732b2bbb-ee7f-4bb2-85b1-e6478955544b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":610617,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"732b2bbb-ee7f-4bb2-85b1-e6478955544b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610617} {\"correlation_id\":\"732b2bbb-ee7f-4bb2-85b1-e6478955544b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610617,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"732b2bbb-ee7f-4bb2-85b1-e6478955544b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":610617,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"732b2bbb-ee7f-4bb2-85b1-e6478955544b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":610539,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"d94f0ac2-c007-4b7d-9fd3-27fc32f9b9a2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610539} {\"correlation_id\":\"d94f0ac2-c007-4b7d-9fd3-27fc32f9b9a2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610539,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"d94f0ac2-c007-4b7d-9fd3-27fc32f9b9a2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":610539,\"participants\":[{\"id\":996485,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":996486,\"user_id\":18,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"d94f0ac2-c007-4b7d-9fd3-27fc32f9b9a2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"d94f0ac2-c007-4b7d-9fd3-27fc32f9b9a2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"d94f0ac2-c007-4b7d-9fd3-27fc32f9b9a2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d94f0ac2-c007-4b7d-9fd3-27fc32f9b9a2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"d94f0ac2-c007-4b7d-9fd3-27fc32f9b9a2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"d94f0ac2-c007-4b7d-9fd3-27fc32f9b9a2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"d94f0ac2-c007-4b7d-9fd3-27fc32f9b9a2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"d94f0ac2-c007-4b7d-9fd3-27fc32f9b9a2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":610539,\"team_id\":2,\"email\":\"veselin.kulov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"d94f0ac2-c007-4b7d-9fd3-27fc32f9b9a2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":610539,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"d94f0ac2-c007-4b7d-9fd3-27fc32f9b9a2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610539} {\"correlation_id\":\"d94f0ac2-c007-4b7d-9fd3-27fc32f9b9a2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610539,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"d94f0ac2-c007-4b7d-9fd3-27fc32f9b9a2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":610539,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"d94f0ac2-c007-4b7d-9fd3-27fc32f9b9a2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":610528,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"c316e103-4e48-4f28-8bc6-206714fc00ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610528} {\"correlation_id\":\"c316e103-4e48-4f28-8bc6-206714fc00ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610528,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"c316e103-4e48-4f28-8bc6-206714fc00ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":610528,\"participants\":[{\"id\":996463,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":996464,\"user_id\":18,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"c316e103-4e48-4f28-8bc6-206714fc00ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"c316e103-4e48-4f28-8bc6-206714fc00ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"c316e103-4e48-4f28-8bc6-206714fc00ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"c316e103-4e48-4f28-8bc6-206714fc00ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"c316e103-4e48-4f28-8bc6-206714fc00ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"c316e103-4e48-4f28-8bc6-206714fc00ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"c316e103-4e48-4f28-8bc6-206714fc00ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"c316e103-4e48-4f28-8bc6-206714fc00ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":610528,\"team_id\":2,\"email\":\"veselin.kulov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"c316e103-4e48-4f28-8bc6-206714fc00ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":610528,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"c316e103-4e48-4f28-8bc6-206714fc00ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610528} {\"correlation_id\":\"c316e103-4e48-4f28-8bc6-206714fc00ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610528,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"c316e103-4e48-4f28-8bc6-206714fc00ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":610528,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"c316e103-4e48-4f28-8bc6-206714fc00ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":610506,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"f83f62c6-544e-4551-8bc2-99f405d022e6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610506} {\"correlation_id\":\"f83f62c6-544e-4551-8bc2-99f405d022e6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610506,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"f83f62c6-544e-4551-8bc2-99f405d022e6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":610506,\"participants\":[{\"id\":996419,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":996420,\"user_id\":18,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"f83f62c6-544e-4551-8bc2-99f405d022e6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"f83f62c6-544e-4551-8bc2-99f405d022e6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"f83f62c6-544e-4551-8bc2-99f405d022e6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"f83f62c6-544e-4551-8bc2-99f405d022e6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"f83f62c6-544e-4551-8bc2-99f405d022e6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"f83f62c6-544e-4551-8bc2-99f405d022e6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"f83f62c6-544e-4551-8bc2-99f405d022e6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"f83f62c6-544e-4551-8bc2-99f405d022e6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1271,\"provider\":\"office\",\"responseBody\":\"{\\\"error\\\":\\\"invalid_client\\\",\\\"error_description\\\":\\\"AADSTS7000215: Invalid client secret provided. Ensure the secret being sent in the request is the client secret value, not the client secret ID, for a secret added to app 'bbcbb2ef-6200-4fae-82bd-d81f5dd738da'. Trace ID: 8addf5ba-9879-4822-b2bb-e5002b0d2200 Correlation ID: feece826-abbe-4c27-85d0-0cddd6b33c3b Timestamp: 2026-05-11 10:43:41Z\\\",\\\"error_codes\\\":[7000215],\\\"timestamp\\\":\\\"2026-05-11 10:43:41Z\\\",\\\"trace_id\\\":\\\"8addf5ba-9879-4822-b2bb-e5002b0d2200\\\",\\\"correlation_id\\\":\\\"feece826-abbe-4c27-85d0-0cddd6b33c3b\\\",\\\"error_uri\\\":\\\"https://login.microsoftonline.com/error?code=7000215\\\"}\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1271,\"provider\":\"office\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":610506,\"team_id\":2,\"email\":\"veselin.kulov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"f83f62c6-544e-4551-8bc2-99f405d022e6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":610506,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"f83f62c6-544e-4551-8bc2-99f405d022e6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610506} {\"correlation_id\":\"f83f62c6-544e-4551-8bc2-99f405d022e6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610506,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"f83f62c6-544e-4551-8bc2-99f405d022e6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1351,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":610506,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"f83f62c6-544e-4551-8bc2-99f405d022e6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1351,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1351,\"provider\":\"google\",\"refreshToken\":\"4271d15b9e60a606439caddc68337f783e472c85b03dacff14d1b6dfded9051c\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":610497,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"854a8b20-3fc0-4ea6-a7c0-e0ddd599dd3b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610497} {\"correlation_id\":\"854a8b20-3fc0-4ea6-a7c0-e0ddd599dd3b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610497,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"854a8b20-3fc0-4ea6-a7c0-e0ddd599dd3b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":610497,\"participants\":[{\"id\":996401,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":996402,\"user_id\":18,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"854a8b20-3fc0-4ea6-a7c0-e0ddd599dd3b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1351,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Bad Request\"}} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1351,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"854a8b20-3fc0-4ea6-a7c0-e0ddd599dd3b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"854a8b20-3fc0-4ea6-a7c0-e0ddd599dd3b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"854a8b20-3fc0-4ea6-a7c0-e0ddd599dd3b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"854a8b20-3fc0-4ea6-a7c0-e0ddd599dd3b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1366,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1366,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"854a8b20-3fc0-4ea6-a7c0-e0ddd599dd3b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"854a8b20-3fc0-4ea6-a7c0-e0ddd599dd3b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1366,\"provider\":\"google\",\"refreshToken\":\"ae21385059b2eebfd43f68aecd56eccd702a1aabb6598f1f7ab594ed8af491b4\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"854a8b20-3fc0-4ea6-a7c0-e0ddd599dd3b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":610497,\"team_id\":2,\"email\":\"veselin.kulov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"854a8b20-3fc0-4ea6-a7c0-e0ddd599dd3b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":610497,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"854a8b20-3fc0-4ea6-a7c0-e0ddd599dd3b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610497} {\"correlation_id\":\"854a8b20-3fc0-4ea6-a7c0-e0ddd599dd3b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610497,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"854a8b20-3fc0-4ea6-a7c0-e0ddd599dd3b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":610497,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"854a8b20-3fc0-4ea6-a7c0-e0ddd599dd3b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":610490,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"bab4020c-8e0d-42eb-afdb-e6bab67d42c7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610490} {\"correlation_id\":\"bab4020c-8e0d-42eb-afdb-e6bab67d42c7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610490,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"bab4020c-8e0d-42eb-afdb-e6bab67d42c7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":610490,\"participants\":[{\"id\":996385,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":996386,\"user_id\":18,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"bab4020c-8e0d-42eb-afdb-e6bab67d42c7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"bab4020c-8e0d-42eb-afdb-e6bab67d42c7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"bab4020c-8e0d-42eb-afdb-e6bab67d42c7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"bab4020c-8e0d-42eb-afdb-e6bab67d42c7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"bab4020c-8e0d-42eb-afdb-e6bab67d42c7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"bab4020c-8e0d-42eb-afdb-e6bab67d42c7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"bab4020c-8e0d-42eb-afdb-e6bab67d42c7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"bab4020c-8e0d-42eb-afdb-e6bab67d42c7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":610490,\"team_id\":2,\"email\":\"veselin.kulov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"bab4020c-8e0d-42eb-afdb-e6bab67d42c7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":610490,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"bab4020c-8e0d-42eb-afdb-e6bab67d42c7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610490} {\"correlation_id\":\"bab4020c-8e0d-42eb-afdb-e6bab67d42c7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610490,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"bab4020c-8e0d-42eb-afdb-e6bab67d42c7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":610490,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"bab4020c-8e0d-42eb-afdb-e6bab67d42c7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1366,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Bad Request\"}} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1366,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":610470,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"55223d65-4d62-42ac-994b-ae4c8f27cbef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1115,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610470} {\"correlation_id\":\"55223d65-4d62-42ac-994b-ae4c8f27cbef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1115,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610470,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"55223d65-4d62-42ac-994b-ae4c8f27cbef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":610470,\"participants\":[{\"id\":996369,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":996370,\"user_id\":18,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"55223d65-4d62-42ac-994b-ae4c8f27cbef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: Calendar sync job dispatched {\"calendar_id\":378} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1421,\"provider\":\"office\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1421,\"provider\":\"office\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"55223d65-4d62-42ac-994b-ae4c8f27cbef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"55223d65-4d62-42ac-994b-ae4c8f27cbef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"55223d65-4d62-42ac-994b-ae4c8f27cbef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: Calendar sync job dispatched {\"calendar_id\":504} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"55223d65-4d62-42ac-994b-ae4c8f27cbef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"55223d65-4d62-42ac-994b-ae4c8f27cbef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"55223d65-4d62-42ac-994b-ae4c8f27cbef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"55223d65-4d62-42ac-994b-ae4c8f27cbef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.NOTICE: Calendar sync end {\"retrieved_calendars\":31,\"processed_calendars\":3} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"calendar:sync\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":610470,\"team_id\":2,\"email\":\"veselin.kulov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"55223d65-4d62-42ac-994b-ae4c8f27cbef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":610470,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"55223d65-4d62-42ac-994b-ae4c8f27cbef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610470} {\"correlation_id\":\"55223d65-4d62-42ac-994b-ae4c8f27cbef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610470,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"55223d65-4d62-42ac-994b-ae4c8f27cbef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":610470,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"55223d65-4d62-42ac-994b-ae4c8f27cbef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":610462,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"0108a9c2-fb98-43fc-ad71-084c53b40936\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610462} {\"correlation_id\":\"0108a9c2-fb98-43fc-ad71-084c53b40936\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610462,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"0108a9c2-fb98-43fc-ad71-084c53b40936\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":610462,\"participants\":[{\"id\":996353,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":996354,\"user_id\":18,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"0108a9c2-fb98-43fc-ad71-084c53b40936\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"0108a9c2-fb98-43fc-ad71-084c53b40936\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"0108a9c2-fb98-43fc-ad71-084c53b40936\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"0108a9c2-fb98-43fc-ad71-084c53b40936\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"0108a9c2-fb98-43fc-ad71-084c53b40936\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"0108a9c2-fb98-43fc-ad71-084c53b40936\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"0108a9c2-fb98-43fc-ad71-084c53b40936\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"0108a9c2-fb98-43fc-ad71-084c53b40936\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":610462,\"team_id\":2,\"email\":\"veselin.kulov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"0108a9c2-fb98-43fc-ad71-084c53b40936\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":610462,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"0108a9c2-fb98-43fc-ad71-084c53b40936\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610462} {\"correlation_id\":\"0108a9c2-fb98-43fc-ad71-084c53b40936\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610462,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"0108a9c2-fb98-43fc-ad71-084c53b40936\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":610462,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"0108a9c2-fb98-43fc-ad71-084c53b40936\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":610451,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"d1fa1d3a-52aa-496e-8905-56f7157299b1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610451} {\"correlation_id\":\"d1fa1d3a-52aa-496e-8905-56f7157299b1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610451,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"d1fa1d3a-52aa-496e-8905-56f7157299b1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":610451,\"participants\":[{\"id\":996340,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":996341,\"user_id\":18,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"d1fa1d3a-52aa-496e-8905-56f7157299b1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"d1fa1d3a-52aa-496e-8905-56f7157299b1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"d1fa1d3a-52aa-496e-8905-56f7157299b1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d1fa1d3a-52aa-496e-8905-56f7157299b1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"d1fa1d3a-52aa-496e-8905-56f7157299b1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"d1fa1d3a-52aa-496e-8905-56f7157299b1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"d1fa1d3a-52aa-496e-8905-56f7157299b1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"d1fa1d3a-52aa-496e-8905-56f7157299b1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":610451,\"team_id\":2,\"email\":\"veselin.kulov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"d1fa1d3a-52aa-496e-8905-56f7157299b1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":610451,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"d1fa1d3a-52aa-496e-8905-56f7157299b1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610451} {\"correlation_id\":\"d1fa1d3a-52aa-496e-8905-56f7157299b1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610451,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"d1fa1d3a-52aa-496e-8905-56f7157299b1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":610451,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"d1fa1d3a-52aa-496e-8905-56f7157299b1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":610438,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"da3f7962-a65d-4bfe-80bf-c33b4ccd36f5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610438} {\"correlation_id\":\"da3f7962-a65d-4bfe-80bf-c33b4ccd36f5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610438,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"da3f7962-a65d-4bfe-80bf-c33b4ccd36f5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":610438,\"participants\":[{\"id\":996320,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":996321,\"user_id\":18,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"da3f7962-a65d-4bfe-80bf-c33b4ccd36f5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"da3f7962-a65d-4bfe-80bf-c33b4ccd36f5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"da3f7962-a65d-4bfe-80bf-c33b4ccd36f5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"da3f7962-a65d-4bfe-80bf-c33b4ccd36f5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"da3f7962-a65d-4bfe-80bf-c33b4ccd36f5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"da3f7962-a65d-4bfe-80bf-c33b4ccd36f5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"da3f7962-a65d-4bfe-80bf-c33b4ccd36f5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"da3f7962-a65d-4bfe-80bf-c33b4ccd36f5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":610438,\"team_id\":2,\"email\":\"veselin.kulov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"da3f7962-a65d-4bfe-80bf-c33b4ccd36f5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":610438,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"da3f7962-a65d-4bfe-80bf-c33b4ccd36f5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610438} {\"correlation_id\":\"da3f7962-a65d-4bfe-80bf-c33b4ccd36f5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610438,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"da3f7962-a65d-4bfe-80bf-c33b4ccd36f5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":610438,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"da3f7962-a65d-4bfe-80bf-c33b4ccd36f5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":610426,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"da7b40c8-e233-4cf9-a133-31f12397ad10\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610426} {\"correlation_id\":\"da7b40c8-e233-4cf9-a133-31f12397ad10\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610426,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"da7b40c8-e233-4cf9-a133-31f12397ad10\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":610426,\"participants\":[{\"id\":996306,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":996307,\"user_id\":18,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"da7b40c8-e233-4cf9-a133-31f12397ad10\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"da7b40c8-e233-4cf9-a133-31f12397ad10\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"da7b40c8-e233-4cf9-a133-31f12397ad10\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"da7b40c8-e233-4cf9-a133-31f12397ad10\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"da7b40c8-e233-4cf9-a133-31f12397ad10\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"da7b40c8-e233-4cf9-a133-31f12397ad10\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"da7b40c8-e233-4cf9-a133-31f12397ad10\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"da7b40c8-e233-4cf9-a133-31f12397ad10\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":610426,\"team_id\":2,\"email\":\"veselin.kulov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"da7b40c8-e233-4cf9-a133-31f12397ad10\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":610426,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"da7b40c8-e233-4cf9-a133-31f12397ad10\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610426} {\"correlation_id\":\"da7b40c8-e233-4cf9-a133-31f12397ad10\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610426,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"da7b40c8-e233-4cf9-a133-31f12397ad10\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":610426,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"da7b40c8-e233-4cf9-a133-31f12397ad10\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":610403,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"4e1aa6ad-3e68-46fe-8129-19feb67cee5f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610403} {\"correlation_id\":\"4e1aa6ad-3e68-46fe-8129-19feb67cee5f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610403,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"4e1aa6ad-3e68-46fe-8129-19feb67cee5f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":610403,\"participants\":[{\"id\":996282,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":996283,\"user_id\":18,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"4e1aa6ad-3e68-46fe-8129-19feb67cee5f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"4e1aa6ad-3e68-46fe-8129-19feb67cee5f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"4e1aa6ad-3e68-46fe-8129-19feb67cee5f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4e1aa6ad-3e68-46fe-8129-19feb67cee5f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"4e1aa6ad-3e68-46fe-8129-19feb67cee5f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"4e1aa6ad-3e68-46fe-8129-19feb67cee5f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"4e1aa6ad-3e68-46fe-8129-19feb67cee5f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"4e1aa6ad-3e68-46fe-8129-19feb67cee5f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":610403,\"team_id\":2,\"email\":\"veselin.kulov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"4e1aa6ad-3e68-46fe-8129-19feb67cee5f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":610403,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"4e1aa6ad-3e68-46fe-8129-19feb67cee5f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610403} {\"correlation_id\":\"4e1aa6ad-3e68-46fe-8129-19feb67cee5f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610403,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"4e1aa6ad-3e68-46fe-8129-19feb67cee5f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":610403,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"4e1aa6ad-3e68-46fe-8129-19feb67cee5f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":610400,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":350,\"stage_id\":34}} {\"correlation_id\":\"c137d466-9e9c-4af1-82c0-9f8639805989\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610400} {\"correlation_id\":\"c137d466-9e9c-4af1-82c0-9f8639805989\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610400,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"c137d466-9e9c-4af1-82c0-9f8639805989\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":610400,\"participants\":[{\"id\":996275,\"user_id\":1460,\"contact_id\":null,\"lead_id\":null},{\"id\":996276,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":996277,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null}]} {\"correlation_id\":\"c137d466-9e9c-4af1-82c0-9f8639805989\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"c137d466-9e9c-4af1-82c0-9f8639805989\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"c137d466-9e9c-4af1-82c0-9f8639805989\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"c137d466-9e9c-4af1-82c0-9f8639805989\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"c137d466-9e9c-4af1-82c0-9f8639805989\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":610400,\"team_id\":2,\"email\":\"aneliya.angelova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"c137d466-9e9c-4af1-82c0-9f8639805989\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":610400,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"c137d466-9e9c-4af1-82c0-9f8639805989\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: ProspectCache - Searching DB for opportunity by owner {\"account_id\":244,\"contact_id\":4487,\"owner_id\":1460} {\"correlation_id\":\"c137d466-9e9c-4af1-82c0-9f8639805989\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: ProspectCache - Fallback DB opportunity search {\"account_id\":244,\"contact_id\":4487} {\"correlation_id\":\"c137d466-9e9c-4af1-82c0-9f8639805989\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: ProspectCache - Opportunity DB search results {\"account_id\":244,\"contact_id\":4487,\"opportunity_id\":350} {\"correlation_id\":\"c137d466-9e9c-4af1-82c0-9f8639805989\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"c137d466-9e9c-4af1-82c0-9f8639805989\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":610400,\"participants_processed\":3,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"c137d466-9e9c-4af1-82c0-9f8639805989\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610400} {\"correlation_id\":\"c137d466-9e9c-4af1-82c0-9f8639805989\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610400,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"c137d466-9e9c-4af1-82c0-9f8639805989\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":610400,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":350,\"stage_id\":34} {\"correlation_id\":\"c137d466-9e9c-4af1-82c0-9f8639805989\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1115,\"provider\":\"google\"} {\"correlation_id\":\"003a1645-a7b4-447f-8217-b1f109d902a1\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:42] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1115,\"provider\":\"google\"} {\"correlation_id\":\"003a1645-a7b4-447f-8217-b1f109d902a1\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:42] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"003a1645-a7b4-447f-8217-b1f109d902a1\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:42] local.INFO: [Calendar] Processing sync {\"calendarId\":\"2676cb6d-f86c-427e-bf78-591e388e3c1e\",\"from\":null,\"to\":null,\"delta\":\"CJ_x49O3jpIDEJ_x49O3jpIDGAUgw67KlwMow67KlwM=\",\"last_sync\":\"2026-01-19 07:48:40\",\"dateMode\":\"daily\"} {\"correlation_id\":\"003a1645-a7b4-447f-8217-b1f109d902a1\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:42] local.WARNING: [Pipedrive] Account not connected for user {\"userId\":\"e6538737-e7b4-455f-a37a-3e79b665a220\",\"account\":{\"Jiminny\\\\Models\\\\SocialAccount\":{\"id\":1116,\"sociable_id\":241,\"provider_user_id\":\"19555731\",\"expires\":1775683749,\"refresh_token_expires\":null,\"provider\":\"pipedrive\",\"state\":\"full-refresh\",\"auth_scope\":\"base,deals:full,activities:full,contacts:full,search:read\",\"retry_after\":null,\"created_at\":\"2023-09-08 09:44:29\",\"updated_at\":\"2026-04-08 22:58:34\"}}} {\"correlation_id\":\"003a1645-a7b4-447f-8217-b1f109d902a1\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:42] local.INFO: [CrmOwnerResolver] Integration owner is not connected, attempting team members {\"crm_provider\":\"pipedrive\",\"crm_owner\":241,\"team_id\":19} {\"correlation_id\":\"003a1645-a7b4-447f-8217-b1f109d902a1\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:42] local.INFO: [CrmOwnerResolver] No team members found with active crm connection {\"crm_provider\":\"pipedrive\",\"team_id\":19} {\"correlation_id\":\"003a1645-a7b4-447f-8217-b1f109d902a1\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:42] local.INFO: [CrmOwnerResolver] No team member found with active crm connection {\"crm_provider\":\"pipedrive\",\"team_id\":19} {\"correlation_id\":\"003a1645-a7b4-447f-8217-b1f109d902a1\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:42] local.WARNING: [Calendar] CRM disconnected for user so events will not be matched {\"provider\":\"pipedrive\",\"user_id\":241,\"message\":\"Your Pipedrive account has become disconnected. Please login to Jiminny to reconnect.\"} {\"correlation_id\":\"003a1645-a7b4-447f-8217-b1f109d902a1\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:42] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1115,\"provider\":\"google\"} {\"correlation_id\":\"003a1645-a7b4-447f-8217-b1f109d902a1\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:42] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1115,\"provider\":\"google\"} {\"correlation_id\":\"003a1645-a7b4-447f-8217-b1f109d902a1\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:42] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"003a1645-a7b4-447f-8217-b1f109d902a1\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:42] local.INFO: [Google Calendar] Failed to watch channel for calendar {\"calendarId\":\"2676cb6d-f86c-427e-bf78-591e388e3c1e\",\"code\":400,\"reason\":\"{\n \\\"error\\\": {\n \\\"errors\\\": [\n {\n \\\"domain\\\": \\\"global\\\",\n \\\"reason\\\": \\\"push.webhookUrlNotHttps\\\",\n \\\"message\\\": \\\"WebHook callback must be HTTPS: /webhook/calendar/google?resourceType=event\\\"\n }\n ],\n \\\"code\\\": 400,\n \\\"message\\\": \\\"WebHook callback must be HTTPS: /webhook/calendar/google?resourceType=event\\\"\n }\n}\"} {\"correlation_id\":\"003a1645-a7b4-447f-8217-b1f109d902a1\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:42] local.WARNING: [Calendar] Sync failed {\"calendarId\":\"2676cb6d-f86c-427e-bf78-591e388e3c1e\",\"code\":400,\"reason\":\"{\n \\\"error\\\": {\n \\\"errors\\\": [\n {\n \\\"domain\\\": \\\"global\\\",\n \\\"reason\\\": \\\"push.webhookUrlNotHttps\\\",\n \\\"message\\\": \\\"WebHook callback must be HTTPS: /webhook/calendar/google?resourceType=event\\\"\n }\n ],\n \\\"code\\\": 400,\n \\\"message\\\": \\\"WebHook callback must be HTTPS: /webhook/calendar/google?resourceType=event\\\"\n }\n}\"} {\"correlation_id\":\"003a1645-a7b4-447f-8217-b1f109d902a1\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:42] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1421,\"provider\":\"office\"} {\"correlation_id\":\"f66a7935-f1a6-4d93-b53d-1464e6e79019\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:42] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1421,\"provider\":\"office\"} {\"correlation_id\":\"f66a7935-f1a6-4d93-b53d-1464e6e79019\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:42] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"f66a7935-f1a6-4d93-b53d-1464e6e79019\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:42] local.INFO: [Calendar] Processing sync {\"calendarId\":\"9e8b1a2c-1a8f-42bd-b161-810fc0baf540\",\"from\":null,\"to\":null,\"delta\":\"R0usmcdvmMuZCBYV0hguCLlvcOB4kXlhlC7KgH1SnZwTrZ3faZv1fXPQqJhxe_L9AxWWlb-wASsjGiiWlhsBUg9MFb3ZdlAYerVV_ZirRPbsKWCxEXhybD90arJmok_M4ecGFUQ9_BIGu-c6RAnJy2TRKZ7gPTsJi_8TGceGAuqimlhm4G4mjDLvYVVwImjjU7M3xJvUzL47dLOGNTJCww.k1TST0VEYCgbFOkwa3ysYMi100FtVfkzfqlXLnV6gPg\",\"last_sync\":\"2026-05-11 06:13:36\",\"dateMode\":\"daily\"} {\"correlation_id\":\"f66a7935-f1a6-4d93-b53d-1464e6e79019\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:42] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"f66a7935-f1a6-4d93-b53d-1464e6e79019\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:42] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"f66a7935-f1a6-4d93-b53d-1464e6e79019\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:42] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"f66a7935-f1a6-4d93-b53d-1464e6e79019\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:42] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":89,\"team_id\":2} {\"correlation_id\":\"f66a7935-f1a6-4d93-b53d-1464e6e79019\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:42] local.INFO: [MS Office Calendar] Skipping delta sync for daily mode {\"calendarId\":\"9e8b1a2c-1a8f-42bd-b161-810fc0baf540\"} {\"correlation_id\":\"f66a7935-f1a6-4d93-b53d-1464e6e79019\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}","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}]...
|
879150276765577717
|
5585982875328481636
|
visual_change
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
3
70
3
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\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 Illuminate\Support\Facades\Redis;
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)
{
$cacheKey = $this->getRateLimitCacheKey();
$cachedRetryAfter = Redis::get($cacheKey);
if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {
throw new RateLimitException(
'Hubspot rate limit (cached circuit-breaker)',
(int) $cachedRetryAfter,
);
}
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'policy' => $this->parsePolicy($e),
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function getRateLimitCacheKey(): string
{
return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());
}
public function isHubspotRateLimit(Throwable $e): bool
{
if ($e instanceof BadRequest
|| $e instanceof DealApiException
|| $e instanceof ContactApiException
|| $e instanceof CompanyApiException
|| $e instanceof \GuzzleHttp\Exception\RequestException
) {
return (int) $e->getCode() === 429;
}
return false;
}
public function parseRetryAfter(Throwable $e): int
{
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$e ' . PHP_EOL . print_r($e, true));
return 5;
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;
}
}
$policy = $this->parsePolicy($e);
if ($policy === 'TEN_SECONDLY_ROLLING') {
return 10;
}
if ($policy === 'SECONDLY') {
return 1;
}
if ($policy === 'DAILY_LIMIT') {
return 600;
}
$this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [
'exception_class' => get_class($e),
]);
return 10;
}
public function parsePolicy(Throwable $e): ?string
{
if (! method_exists($e, 'getResponseBody')) {
return null;
}
$body = $e->getResponseBody();
if (is_string($body)) {
$body = json_decode($body, true) ?? [];
}
if (! is_array($body)) {
return null;
}
$policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;
return is_string($policy) ? strtoupper($policy) : null;
}
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
);
}
/**
* Execute a search request against HubSpot CRM objects with rate limiting.
*
* @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')
* @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.
* @return array The search response with 'results', 'total', 'paging' keys
* @throws RateLimitException When rate limit is hit
* @throws HubspotException On API errors
*/
public function search(string $objectType, array $payload): array
{
$endpoint = self::BASE_URL . "/crm/v3/objects/{$objectType}/search";
return $this->executeRequest(function () use ($endpoint, $payload) {
$response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$response ' . PHP_EOL . print_r($response, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$headers ' . PHP_EOL . print_r($response->getHeaders(), true));
return $response->toArray();
});
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$deal = $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') {
return $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
return $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
}
/**
* @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 (RateLimitException $e) {
// throw $e;
} 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);
}
}
Show Replace Field
Search History
Received 429 from API
New Line
Match Case
Words
Regex
Replace History
Replace
New Line
Preserve case
0 results
Previous Occurrence
Next Occurrence
Filter Search Results
Open in Window, Multiple Cursors
Click to highlight
Close
Sync Changes
Hide This Notification
Code changed:
Hide
[2026-05-11 10:41:16] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"mailbox:batch:process","memoryBeforeCommandInMb":60.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"ee9b9096-b1f1-4e2b-ac17-af0003eb00c0","trace_id":"8904c61b-d5b4-44cc-91bf-68f0f73222c0"}
[2026-05-11 10:41:16] local.INFO: [EmailSchedule] STARTING batch process {"host":"docker_lamp_1"} {"correlation_id":"ee9b9096-b1f1-4e2b-ac17-af0003eb00c0","trace_id":"8904c61b-d5b4-44cc-91bf-68f0f73222c0"}
[2026-05-11 10:41:17] local.INFO: [EmailSchedule] FINISHED batch process {"host":"docker_lamp_1","processed":0} {"correlation_id":"ee9b9096-b1f1-4e2b-ac17-af0003eb00c0","trace_id":"8904c61b-d5b4-44cc-91bf-68f0f73222c0"}
[2026-05-11 10:41:17] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"mailbox:batch:process","memoryBeforeCommandInMb":60.0,"memoryAfterCommandInMB":60.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"ee9b9096-b1f1-4e2b-ac17-af0003eb00c0","trace_id":"8904c61b-d5b4-44cc-91bf-68f0f73222c0"}
[2026-05-11 10:41:21] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"crm:sync-hubspot-objects","memoryBeforeCommandInMb":60.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"13c8d2de-a598-41db-b4ca-9799a4bee946","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:21] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"crm:sync-hubspot-objects","memoryBeforeCommandInMb":60.0,"memoryAfterCommandInMB":60.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"13c8d2de-a598-41db-b4ca-9799a4bee946","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:21] local.INFO: [SyncHubspotObjects] Starting sync {"team":"b2b115eb-93ce-4d1b-929c-173757df8fba","usage":24894088,"real_usage":65011712,"pid":62058} {"correlation_id":"81433689-3fcd-4128-8b21-068ce5ee75be","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:21] local.WARNING: [HubSpot] Account not connected for user {"userId":"33e34a7a-1c02-4f04-87ac-22c3a385e6e3","account":{"Jiminny\\Models\\SocialAccount":{"id":306,"sociable_id":109,"provider_user_id":"11348452","expires":1701077403,"refresh_token_expires":null,"provider":"hubspot","state":"full-refresh","auth_scope":null,"retry_after":null,"created_at":"2020-09-01 16:59:04","updated_at":"2023-11-27 09:30:03"}}} {"correlation_id":"81433689-3fcd-4128-8b21-068ce5ee75be","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:21] local.INFO: [CrmOwnerResolver] Integration owner is not connected, attempting team members {"crm_provider":"hubspot","crm_owner":109,"team_id":29} {"correlation_id":"81433689-3fcd-4128-8b21-068ce5ee75be","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:21] local.INFO: [CrmOwnerResolver] No team members found with active crm connection {"crm_provider":"hubspot","team_id":29} {"correlation_id":"81433689-3fcd-4128-8b21-068ce5ee75be","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:21] local.INFO: [CrmOwnerResolver] No team member found with active crm connection {"crm_provider":"hubspot","team_id":29} {"correlation_id":"81433689-3fcd-4128-8b21-068ce5ee75be","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:21] local.INFO: [SyncHubspotObjects] Sync finished {"team":"b2b115eb-93ce-4d1b-929c-173757df8fba","provider":"hubspot","status":"disconnected","duration_ms":35.85,"usage":24956128,"real_usage":65011712,"pid":62058,"reason":"Your HubSpot account has become disconnected. Please login to Jiminny to reconnect."} {"correlation_id":"81433689-3fcd-4128-8b21-068ce5ee75be","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:21] local.INFO: [SyncHubspotObjects] Starting sync {"team":"abae74b8-bfa8-4383-9a7f-89f4bf2bdbb4","usage":24914344,"real_usage":65011712,"pid":62058} {"correlation_id":"d2cb4aff-14c0-474f-89f2-f5c69dafe934","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:21] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1499,"provider":"hubspot"} {"correlation_id":"d2cb4aff-14c0-474f-89f2-f5c69dafe934","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:21] local.INFO: [SocialAccountService] Token retrieved {"socialAccountId":1499,"provider":"hubspot"} {"correlation_id":"d2cb4aff-14c0-474f-89f2-f5c69dafe934","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:21] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"d2cb4aff-14c0-474f-89f2-f5c69dafe934","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:21] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {"crm_provider":"hubspot","crm_owner":148,"team_id":2} {"correlation_id":"d2cb4aff-14c0-474f-89f2-f5c69dafe934","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:21] local.INFO: [HubSpot] Syncing opportunities using strategy: lastModified {"team":2} {"correlation_id":"d2cb4aff-14c0-474f-89f2-f5c69dafe934","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:21] local.INFO: [Hubspot] Pagination completed {"team_id":2,"endpoint":"https://api.hubapi.com/crm/v3/objects/deals/search","total_requests":1,"total_records_fetched":0,"total_elapsed_seconds":0.26,"average_seconds_per_request":0.26} {"correlation_id":"d2cb4aff-14c0-474f-89f2-f5c69dafe934","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:21] local.INFO: [HubSpot] Synced opportunities {"team":2,"strategies":"lastModified","sync_count":0,"total":0,"last_synced_id":null,"duration_ms":288.63} {"correlation_id":"d2cb4aff-14c0-474f-89f2-f5c69dafe934","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:21] local.INFO: [SyncHubspotObjects] Sync finished {"team":"abae74b8-bfa8-4383-9a7f-89f4bf2bdbb4","provider":"hubspot","status":"completed","duration_ms":324.99,"usage":24978696,"real_usage":65011712,"pid":62058} {"correlation_id":"d2cb4aff-14c0-474f-89f2-f5c69dafe934","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:22] local.INFO: [SyncHubspotObjects] Starting sync {"team":"c6b9d6b0-b48d-4832-a68c-a57d60651888","usage":24956624,"real_usage":65011712,"pid":62058} {"correlation_id":"955d9400-2f7f-4170-be31-43bafa1ef3bd","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:22] local.WARNING: [HubSpot] Account not connected for user {"userId":"71e3aac5-fb66-47c5-a236-2d051ae3e319","account":null} {"correlation_id":"955d9400-2f7f-4170-be31-43bafa1ef3bd","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:22] local.INFO: [CrmOwnerResolver] Integration owner is not connected, attempting team members {"crm_provider":"hubspot","crm_owner":256,"team_id":49} {"correlation_id":"955d9400-2f7f-4170-be31-43bafa1ef3bd","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:22] local.INFO: [CrmOwnerResolver] No team members found with active crm connection {"crm_provider":"hubspot","team_id":49} {"correlation_id":"955d9400-2f7f-4170-be31-43bafa1ef3bd","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:22] local.INFO: [CrmOwnerResolver] No team member found with active crm connection {"crm_provider":"hubspot","team_id":49} {"correlation_id":"955d9400-2f7f-4170-be31-43bafa1ef3bd","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:22] local.INFO: [SyncHubspotObjects] Sync finished {"team":"c6b9d6b0-b48d-4832-a68c-a57d60651888","provider":"hubspot","status":"disconnected","duration_ms":17.51,"usage":24941048,"real_usage":65011712,"pid":62058,"reason":"Social account for HubSpot cannot be found. Please login to Jiminny to connect."} {"correlation_id":"955d9400-2f7f-4170-be31-43bafa1ef3bd","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:22] local.INFO: [SyncHubspotObjects] Starting sync {"team":"b2d49a54-b645-4637-a7ae-a86cfce6e8e4","usage":24901688,"real_usage":65011712,"pid":62058} {"correlation_id":"6d0b5717-9160-4c2f-ad12-56e42a2c544a","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:22] local.WARNING: [HubSpot] Account not connected for user {"userId":"2ac0447f-3c8c-4ce0-baeb-b63ddb76fa9b","account":null} {"correlation_id":"6d0b5717-9160-4c2f-ad12-56e42a2c544a","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:22] local.INFO: [CrmOwnerResolver] Integration owner is not connected, attempting team members {"crm_provider":"hubspot","crm_owner":130,"team_id":42} {"correlation_id":"6d0b5717-9160-4c2f-ad12-56e42a2c544a","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:22] local.INFO: [CrmOwnerResolver] No team members found with active crm connection {"crm_provider":"hubspot","team_id":42} {"correlation_id":"6d0b5717-9160-4c2f-ad12-56e42a2c544a","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:22] local.INFO: [CrmOwnerResolver] No team member found with active crm connection {"crm_provider":"hubspot","team_id":42} {"correlation_id":"6d0b5717-9160-4c2f-ad12-56e42a2c544a","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:22] local.INFO: [SyncHubspotObjects] Sync finished {"team":"b2d49a54-b645-4637-a7ae-a86cfce6e8e4","provider":"hubspot","status":"disconnected","duration_ms":10.99,"usage":24944632,"real_usage":65011712,"pid":62058,"reason":"Social account for HubSpot cannot be found. Please login to Jiminny to connect."} {"correlation_id":"6d0b5717-9160-4c2f-ad12-56e42a2c544a","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:39] local.INFO: [HubSpot Journal Polling] No data {"correlation_id":"1f8f11e6-0cf5-49dd-b8d7-56f51ec27f8e","trace_id":"0f08076e-448d-487e-99a0-e73dae5c0ad4"}
[2026-05-11 10:41:39] local.WARNING: [HubSpot Journal Polling] Maximum empty results reached, stopping {"empty_results":5,"max_empty_results":5} {"correlation_id":"1f8f11e6-0cf5-49dd-b8d7-56f51ec27f8e","trace_id":"0f08076e-448d-487e-99a0-e73dae5c0ad4"}
[2026-05-11 10:41:39] local.WARNING: [HubSpot Journal Polling] Maximum empty results reached, stopping {"empty_results":5,"max_empty_results":5} {"correlation_id":"1f8f11e6-0cf5-49dd-b8d7-56f51ec27f8e","trace_id":"0f08076e-448d-487e-99a0-e73dae5c0ad4"}
[2026-05-11 10:41:39] local.INFO: [HubSpot Journal Polling] Service ending {"runtime_seconds":57,"total_cycles":5,"files_downloaded":0,"empty_files":0,"other_portal_skipped":0,"total_events":0,"events_per_file":0,"avg_api_ms":296.8,"avg_download_ms":0.0,"avg_transform_ms":0.0,"avg_process_ms":0.0,"peak_memory_mb":99.73} {"correlation_id":"1f8f11e6-0cf5-49dd-b8d7-56f51ec27f8e","trace_id":"0f08076e-448d-487e-99a0-e73dae5c0ad4"}
[2026-05-11 10:41:39] local.INFO: [HubSpot Journal Polling] Saved offset to database on cleanup {"offset":"019e15a9-9ea0-7da7-87bc-82592e3ccf0d"} {"correlation_id":"1f8f11e6-0cf5-49dd-b8d7-56f51ec27f8e","trace_id":"0f08076e-448d-487e-99a0-e73dae5c0ad4"}
[2026-05-11 10:41:39] local.INFO: [HubSpot Journal Polling] Released polling lock {"correlation_id":"1f8f11e6-0cf5-49dd-b8d7-56f51ec27f8e","trace_id":"0f08076e-448d-487e-99a0-e73dae5c0ad4"}
[2026-05-11 10:42:07] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"meeting-bot:schedule-bot","memoryBeforeCommandInMb":60.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"1aa66921-0a71-4d61-8cd7-0663242dca44","trace_id":"f6b1d739-39b6-4043-a81e-a1af2c76d401"}
[2026-05-11 10:42:07] local.INFO: [ScheduleBotCommand] Number of activities to be captured: 0 {"correlation_id":"1aa66921-0a71-4d61-8cd7-0663242dca44","trace_id":"f6b1d739-39b6-4043-a81e-a1af2c76d401"}
[2026-05-11 10:42:07] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"meeting-bot:schedule-bot","memoryBeforeCommandInMb":60.0,"memoryAfterCommandInMB":60.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"1aa66921-0a71-4d61-8cd7-0663242dca44","trace_id":"f6b1d739-39b6-4043-a81e-a1af2c76d401"}
[2026-05-11 10:42:10] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"dialers:monitor-activities","memoryBeforeCommandInMb":60.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"cc420ba5-39bf-4183-95ae-48962a9c124b","trace_id":"8daf6081-5c8a-4fae-98d8-e317d8580bc3"}
[2026-05-11 10:42:10] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"dialers:monitor-activities","memoryBeforeCommandInMb":60.0,"memoryAfterCommandInMB":60.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"cc420ba5-39bf-4183-95ae-48962a9c124b","trace_id":"8daf6081-5c8a-4fae-98d8-e317d8580bc3"}
[2026-05-11 10:42:13] local.NOTICE: Monitoring start {"correlation_id":"50696062-587c-4c43-8075-7229916ea46f","trace_id":"b58f754c-906f-413b-80e6-f89189dbe078"}
[2026-05-11 10:42:13] local.NOTICE: Monitoring end {"correlation_id":"50696062-587c-4c43-8075-7229916ea46f","trace_id":"b58f754c-906f-413b-80e6-f89189dbe078"}
[2026-05-11 10:42:17] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"mailbox:skip-lists:refresh","memoryBeforeCommandInMb":60.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"f70656cd-6de3-4f8b-bb9d-74a941c552c4","trace_id":"c780d8e3-2abd-41a3-a9dc-7c2be6aa22d4"}
[2026-05-11 10:42:17] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"mailbox:skip-lists:refresh","memoryBeforeCommandInMb":60.0,"memoryAfterCommandInMB":60.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"f70656cd-6de3-4f8b-bb9d-74a941c552c4","trace_id":"c780d8e3-2abd-41a3-a9dc-7c2be6aa22d4"}
[2026-05-11 10:42:20] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"mailbox:batch:process","memoryBeforeCommandInMb":60.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"b2e73601-1777-4ddb-a152-fe85f4c89afc","trace_id":"8bac5054-216a-4605-b13b-9efab8ab5341"}
[2026-05-11 10:42:20] local.INFO: [EmailSchedule] STARTING batch process {"host":"docker_lamp_1"} {"correlation_id":"b2e73601-1777-4ddb-a152-fe85f4c89afc","trace_id":"8bac5054-216a-4605-b13b-9efab8ab5341"}
[2026-05-11 10:42:20] local.INFO: [EmailSchedule] FINISHED batch process {"host":"docker_lamp_1","processed":0} {"correlation_i...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
17970
|
782
|
10
|
2026-05-11T10:43:49.141933+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778496229141_m1.jpg...
|
PhpStorm
|
faVsco.js – laravel.log
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
3
70
3
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\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 Illuminate\Support\Facades\Redis;
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)
{
$cacheKey = $this->getRateLimitCacheKey();
$cachedRetryAfter = Redis::get($cacheKey);
if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {
throw new RateLimitException(
'Hubspot rate limit (cached circuit-breaker)',
(int) $cachedRetryAfter,
);
}
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'policy' => $this->parsePolicy($e),
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function getRateLimitCacheKey(): string
{
return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());
}
public function isHubspotRateLimit(Throwable $e): bool
{
if ($e instanceof BadRequest
|| $e instanceof DealApiException
|| $e instanceof ContactApiException
|| $e instanceof CompanyApiException
|| $e instanceof \GuzzleHttp\Exception\RequestException
) {
return (int) $e->getCode() === 429;
}
return false;
}
public function parseRetryAfter(Throwable $e): int
{
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$e ' . PHP_EOL . print_r($e, true));
return 5;
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;
}
}
$policy = $this->parsePolicy($e);
if ($policy === 'TEN_SECONDLY_ROLLING') {
return 10;
}
if ($policy === 'SECONDLY') {
return 1;
}
if ($policy === 'DAILY_LIMIT') {
return 600;
}
$this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [
'exception_class' => get_class($e),
]);
return 10;
}
public function parsePolicy(Throwable $e): ?string
{
if (! method_exists($e, 'getResponseBody')) {
return null;
}
$body = $e->getResponseBody();
if (is_string($body)) {
$body = json_decode($body, true) ?? [];
}
if (! is_array($body)) {
return null;
}
$policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;
return is_string($policy) ? strtoupper($policy) : null;
}
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
);
}
/**
* Execute a search request against HubSpot CRM objects with rate limiting.
*
* @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')
* @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.
* @return array The search response with 'results', 'total', 'paging' keys
* @throws RateLimitException When rate limit is hit
* @throws HubspotException On API errors
*/
public function search(string $objectType, array $payload): array
{
$endpoint = self::BASE_URL . "/crm/v3/objects/{$objectType}/search";
return $this->executeRequest(function () use ($endpoint, $payload) {
$response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$response ' . PHP_EOL . print_r($response, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$headers ' . PHP_EOL . print_r($response->getHeaders(), true));
return $response->toArray();
});
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$deal = $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') {
return $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
return $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
}
/**
* @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 (RateLimitException $e) {
// throw $e;
} 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);
}
}
Show Replace Field
Search History
Received 429 from API
New Line
Match Case
Words
Regex
Replace History
Replace
New Line
Preserve case
0 results
Previous Occurrence
Next Occurrence
Filter Search Results
Open in Window, Multiple Cursors
Click to highlight
Close
Sync Changes
Hide This Notification
Code changed:
Hide
[2026-05-11 10:41:16] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"mailbox:batch:process","memoryBeforeCommandInMb":60.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"ee9b9096-b1f1-4e2b-ac17-af0003eb00c0","trace_id":"8904c61b-d5b4-44cc-91bf-68f0f73222c0"}
[2026-05-11 10:41:16] local.INFO: [EmailSchedule] STARTING batch process {"host":"docker_lamp_1"} {"correlation_id":"ee9b9096-b1f1-4e2b-ac17-af0003eb00c0","trace_id":"8904c61b-d5b4-44cc-91bf-68f0f73222c0"}
[2026-05-11 10:41:17] local.INFO: [EmailSchedule] FINISHED batch process {"host":"docker_lamp_1","processed":0} {"correlation_id":"ee9b9096-b1f1-4e2b-ac17-af0003eb00c0","trace_id":"8904c61b-d5b4-44cc-91bf-68f0f73222c0"}
[2026-05-11 10:41:17] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"mailbox:batch:process","memoryBeforeCommandInMb":60.0,"memoryAfterCommandInMB":60.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"ee9b9096-b1f1-4e2b-ac17-af0003eb00c0","trace_id":"8904c61b-d5b4-44cc-91bf-68f0f73222c0"}
[2026-05-11 10:41:21] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"crm:sync-hubspot-objects","memoryBeforeCommandInMb":60.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"13c8d2de-a598-41db-b4ca-9799a4bee946","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:21] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"crm:sync-hubspot-objects","memoryBeforeCommandInMb":60.0,"memoryAfterCommandInMB":60.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"13c8d2de-a598-41db-b4ca-9799a4bee946","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:21] local.INFO: [SyncHubspotObjects] Starting sync {"team":"b2b115eb-93ce-4d1b-929c-173757df8fba","usage":24894088,"real_usage":65011712,"pid":62058} {"correlation_id":"81433689-3fcd-4128-8b21-068ce5ee75be","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:21] local.WARNING: [HubSpot] Account not connected for user {"userId":"33e34a7a-1c02-4f04-87ac-22c3a385e6e3","account":{"Jiminny\\Models\\SocialAccount":{"id":306,"sociable_id":109,"provider_user_id":"11348452","expires":1701077403,"refresh_token_expires":null,"provider":"hubspot","state":"full-refresh","auth_scope":null,"retry_after":null,"created_at":"2020-09-01 16:59:04","updated_at":"2023-11-27 09:30:03"}}} {"correlation_id":"81433689-3fcd-4128-8b21-068ce5ee75be","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:21] local.INFO: [CrmOwnerResolver] Integration owner is not connected, attempting team members {"crm_provider":"hubspot","crm_owner":109,"team_id":29} {"correlation_id":"81433689-3fcd-4128-8b21-068ce5ee75be","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:21] local.INFO: [CrmOwnerResolver] No team members found with active crm connection {"crm_provider":"hubspot","team_id":29} {"correlation_id":"81433689-3fcd-4128-8b21-068ce5ee75be","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:21] local.INFO: [CrmOwnerResolver] No team member found with active crm connection {"crm_provider":"hubspot","team_id":29} {"correlation_id":"81433689-3fcd-4128-8b21-068ce5ee75be","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:21] local.INFO: [SyncHubspotObjects] Sync finished {"team":"b2b115eb-93ce-4d1b-929c-173757df8fba","provider":"hubspot","status":"disconnected","duration_ms":35.85,"usage":24956128,"real_usage":65011712,"pid":62058,"reason":"Your HubSpot account has become disconnected. Please login to Jiminny to reconnect."} {"correlation_id":"81433689-3fcd-4128-8b21-068ce5ee75be","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:21] local.INFO: [SyncHubspotObjects] Starting sync {"team":"abae74b8-bfa8-4383-9a7f-89f4bf2bdbb4","usage":24914344,"real_usage":65011712,"pid":62058} {"correlation_id":"d2cb4aff-14c0-474f-89f2-f5c69dafe934","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:21] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1499,"provider":"hubspot"} {"correlation_id":"d2cb4aff-14c0-474f-89f2-f5c69dafe934","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:21] local.INFO: [SocialAccountService] Token retrieved {"socialAccountId":1499,"provider":"hubspot"} {"correlation_id":"d2cb4aff-14c0-474f-89f2-f5c69dafe934","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:21] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"d2cb4aff-14c0-474f-89f2-f5c69dafe934","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:21] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {"crm_provider":"hubspot","crm_owner":148,"team_id":2} {"correlation_id":"d2cb4aff-14c0-474f-89f2-f5c69dafe934","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:21] local.INFO: [HubSpot] Syncing opportunities using strategy: lastModified {"team":2} {"correlation_id":"d2cb4aff-14c0-474f-89f2-f5c69dafe934","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:21] local.INFO: [Hubspot] Pagination completed {"team_id":2,"endpoint":"https://api.hubapi.com/crm/v3/objects/deals/search","total_requests":1,"total_records_fetched":0,"total_elapsed_seconds":0.26,"average_seconds_per_request":0.26} {"correlation_id":"d2cb4aff-14c0-474f-89f2-f5c69dafe934","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:21] local.INFO: [HubSpot] Synced opportunities {"team":2,"strategies":"lastModified","sync_count":0,"total":0,"last_synced_id":null,"duration_ms":288.63} {"correlation_id":"d2cb4aff-14c0-474f-89f2-f5c69dafe934","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:21] local.INFO: [SyncHubspotObjects] Sync finished {"team":"abae74b8-bfa8-4383-9a7f-89f4bf2bdbb4","provider":"hubspot","status":"completed","duration_ms":324.99,"usage":24978696,"real_usage":65011712,"pid":62058} {"correlation_id":"d2cb4aff-14c0-474f-89f2-f5c69dafe934","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:22] local.INFO: [SyncHubspotObjects] Starting sync {"team":"c6b9d6b0-b48d-4832-a68c-a57d60651888","usage":24956624,"real_usage":65011712,"pid":62058} {"correlation_id":"955d9400-2f7f-4170-be31-43bafa1ef3bd","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:22] local.WARNING: [HubSpot] Account not connected for user {"userId":"71e3aac5-fb66-47c5-a236-2d051ae3e319","account":null} {"correlation_id":"955d9400-2f7f-4170-be31-43bafa1ef3bd","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:22] local.INFO: [CrmOwnerResolver] Integration owner is not connected, attempting team members {"crm_provider":"hubspot","crm_owner":256,"team_id":49} {"correlation_id":"955d9400-2f7f-4170-be31-43bafa1ef3bd","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:22] local.INFO: [CrmOwnerResolver] No team members found with active crm connection {"crm_provider":"hubspot","team_id":49} {"correlation_id":"955d9400-2f7f-4170-be31-43bafa1ef3bd","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:22] local.INFO: [CrmOwnerResolver] No team member found with active crm connection {"crm_provider":"hubspot","team_id":49} {"correlation_id":"955d9400-2f7f-4170-be31-43bafa1ef3bd","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:22] local.INFO: [SyncHubspotObjects] Sync finished {"team":"c6b9d6b0-b48d-4832-a68c-a57d60651888","provider":"hubspot","status":"disconnected","duration_ms":17.51,"usage":24941048,"real_usage":65011712,"pid":62058,"reason":"Social account for HubSpot cannot be found. Please login to Jiminny to connect."} {"correlation_id":"955d9400-2f7f-4170-be31-43bafa1ef3bd","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:22] local.INFO: [SyncHubspotObjects] Starting sync {"team":"b2d49a54-b645-4637-a7ae-a86cfce6e8e4","usage":24901688,"real_usage":65011712,"pid":62058} {"correlation_id":"6d0b5717-9160-4c2f-ad12-56e42a2c544a","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:22] local.WARNING: [HubSpot] Account not connected for user {"userId":"2ac0447f-3c8c-4ce0-baeb-b63ddb76fa9b","account":null} {"correlation_id":"6d0b5717-9160-4c2f-ad12-56e42a2c544a","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:22] local.INFO: [CrmOwnerResolver] Integration owner is not connected, attempting team members {"crm_provider":"hubspot","crm_owner":130,"team_id":42} {"correlation_id":"6d0b5717-9160-4c2f-ad12-56e42a2c544a","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:22] local.INFO: [CrmOwnerResolver] No team members found with active crm connection {"crm_provider":"hubspot","team_id":42} {"correlation_id":"6d0b5717-9160-4c2f-ad12-56e42a2c544a","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:22] local.INFO: [CrmOwnerResolver] No team member found with active crm connection {"crm_provider":"hubspot","team_id":42} {"correlation_id":"6d0b5717-9160-4c2f-ad12-56e42a2c544a","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:22] local.INFO: [SyncHubspotObjects] Sync finished {"team":"b2d49a54-b645-4637-a7ae-a86cfce6e8e4","provider":"hubspot","status":"disconnected","duration_ms":10.99,"usage":24944632,"real_usage":65011712,"pid":62058,"reason":"Social account for HubSpot cannot be found. Please login to Jiminny to connect."} {"correlation_id":"6d0b5717-9160-4c2f-ad12-56e42a2c544a","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:39] local.INFO: [HubSpot Journal Polling] No data {"correlation_id":"1f8f11e6-0cf5-49dd-b8d7-56f51ec27f8e","trace_id":"0f08076e-448d-487e-99a0-e73dae5c0ad4"}
[2026-05-11 10:41:39] local.WARNING: [HubSpot Journal Polling] Maximum empty results reached, stopping {"empty_results":5,"max_empty_results":5} {"correlation_id":"1f8f11e6-0cf5-49dd-b8d7-56f51ec27f8e","trace_id":"0f08076e-448d-487e-99a0-e73dae5c0ad4"}
[2026-05-11 10:41:39] local.WARNING: [HubSpot Journal Polling] Maximum empty results reached, stopping {"empty_results":5,"max_empty_results":5} {"correlation_id":"1f8f11e6-0cf5-49dd-b8d7-56f51ec27f8e","trace_id":"0f08076e-448d-487e-99a0-e73dae5c0ad4"}
[2026-05-11 10:41:39] local.INFO: [HubSpot Journal Polling] Service ending {"runtime_seconds":57,"total_cycles":5,"files_downloaded":0,"empty_files":0,"other_portal_skipped":0,"total_events":0,"events_per_file":0,"avg_api_ms":296.8,"avg_download_ms":0.0,"avg_transform_ms":0.0,"avg_process_ms":0.0,"peak_memory_mb":99.73} {"correlation_id":"1f8f11e6-0cf5-49dd-b8d7-56f51ec27f8e","trace_id":"0f08076e-448d-487e-99a0-e73dae5c0ad4"}
[2026-05-11 10:41:39] local.INFO: [HubSpot Journal Polling] Saved offset to database on cleanup {"offset":"019e15a9-9ea0-7da7-87bc-82592e3ccf0d"} {"correlation_id":"1f8f11e6-0cf5-49dd-b8d7-56f51ec27f8e","trace_id":"0f08076e-448d-487e-99a0-e73dae5c0ad4"}
[2026-05-11 10:41:39] local.INFO: [HubSpot Journal Polling] Released polling lock {"correlation_id":"1f8f11e6-0cf5-49dd-b8d7-56f51ec27f8e","trace_id":"0f08076e-448d-487e-99a0-e73dae5c0ad4"}
[2026-05-11 10:42:07] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"meeting-bot:schedule-bot","memoryBeforeCommandInMb":60.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"1aa66921-0a71-4d61-8cd7-0663242dca44","trace_id":"f6b1d739-39b6-4043-a81e-a1af2c76d401"}
[2026-05-11 10:42:07] local.INFO: [ScheduleBotCommand] Number of activities to be captured: 0 {"correlation_id":"1aa66921-0a71-4d61-8cd7-0663242dca44","trace_id":"f6b1d739-39b6-4043-a81e-a1af2c76d401"}
[2026-05-11 10:42:07] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"meeting-bot:schedule-bot","memoryBeforeCommandInMb":60.0,"memoryAfterCommandInMB":60.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"1aa66921-0a71-4d61-8cd7-0663242dca44","trace_id":"f6b1d739-39b6-4043-a81e-a1af2c76d401"}
[2026-05-11 10:42:10] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"dialers:monitor-activities","memoryBeforeCommandInMb":60.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"cc420ba5-39bf-4183-95ae-48962a9c124b","trace_id":"8daf6081-5c8a-4fae-98d8-e317d8580bc3"}
[2026-05-11 10:42:10] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"dialers:monitor-activities","memoryBeforeCommandInMb":60.0,"memoryAfterCommandInMB":60.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"cc420ba5-39bf-4183-95ae-48962a9c124b","trace_id":"8daf6081-5c8a-4fae-98d8-e317d8580bc3"}
[2026-05-11 10:42:13] local.NOTICE: Monitoring start {"correlation_id":"50696062-587c-4c43-8075-7229916ea46f","trace_id":"b58f754c-906f-413b-80e6-f89189dbe078"}
[2026-05-11 10:42:13] local.NOTICE: Monitoring end {"correlation_id":"50696062-587c-4c43-8075-7229916ea46f","trace_id":"b58f754c-906f-413b-80e6-f89189dbe078"}
[2026-05-11 10:42:17] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"mailbox:skip-lists:refresh","memoryBeforeCommandInMb":60.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"f70656cd-6de3-4f8b-bb9d-74a941c552c4","trace_id":"c780d8e3-2abd-41a3-a9dc-7c2be6aa22d4"}
[2026-05-11 10:42:17] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"mailbox:skip-lists:refresh","memoryBeforeCommandInMb":60.0,"memoryAfterCommandInMB":60.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"f70656cd-6de3-4f8b-bb9d-74a941c552c4","trace_id":"c780d8e3-2abd-41a3-a9dc-7c2be6aa22d4"}
[2026-05-11 10:42:20] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"mailbox:batch:process","memoryBeforeCommandInMb":60.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"b2e73601-1777-4ddb-a152-fe85f4c89afc","trace_id":"8bac5054-216a-4605-b13b-9efab8ab5341"}
[2026-05-11 10:42:20] local.INFO: [EmailSchedule] STARTING batch process {"host":"docker_lamp_1"} {"correlation_id":"b2e73601-1777-4ddb-a152-fe85f4c89afc","trace_id":"8bac5054-216a-4605-b13b-9efab8ab5341"}
[2026-05-11 10:42:20] local.INFO: [EmailSchedule] FINISHED batch process {"host":"docker_lamp_1","processed":0} {"correlation_i...
|
[{"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":"JY-20725-handle-HS-search-rate-limit, menu","depth":5,"on_screen":true,"help_text":"Git Branch: JY-20725-handle-HS-search-rate-limit","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":"HandleHubspotRateLimitTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'HandleHubspotRateLimitTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'HandleHubspotRateLimitTest'","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":"AXStaticText","text":"3","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"70","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"3","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"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\\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 Illuminate\\Support\\Facades\\Redis;\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\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Reacts to a rate limits (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 $cacheKey = $this->getRateLimitCacheKey();\n\n $cachedRetryAfter = Redis::get($cacheKey);\n if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n (int) $cachedRetryAfter,\n );\n }\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);\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 'policy' => $this->parsePolicy($e),\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 getRateLimitCacheKey(): string\n {\n return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());\n }\n\n public function isHubspotRateLimit(Throwable $e): bool\n {\n if ($e instanceof BadRequest\n || $e instanceof DealApiException\n || $e instanceof ContactApiException\n || $e instanceof CompanyApiException\n || $e instanceof \\GuzzleHttp\\Exception\\RequestException\n ) {\n return (int) $e->getCode() === 429;\n }\n\n return false;\n }\n\n public function parseRetryAfter(Throwable $e): int\n {\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$e ' . PHP_EOL . print_r($e, true));\n return 5;\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 $policy = $this->parsePolicy($e);\n if ($policy === 'TEN_SECONDLY_ROLLING') {\n return 10;\n }\n if ($policy === 'SECONDLY') {\n return 1;\n }\n if ($policy === 'DAILY_LIMIT') {\n return 600;\n }\n\n $this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [\n 'exception_class' => get_class($e),\n ]);\n\n return 10;\n }\n\n public function parsePolicy(Throwable $e): ?string\n {\n if (! method_exists($e, 'getResponseBody')) {\n return null;\n }\n\n $body = $e->getResponseBody();\n if (is_string($body)) {\n $body = json_decode($body, true) ?? [];\n }\n\n if (! is_array($body)) {\n return null;\n }\n\n $policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;\n\n return is_string($policy) ? strtoupper($policy) : null;\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 * Execute a search request against HubSpot CRM objects with rate limiting.\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n * @return array The search response with 'results', 'total', 'paging' keys\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n */\n public function search(string $objectType, array $payload): array\n {\n $endpoint = self::BASE_URL . \"/crm/v3/objects/{$objectType}/search\";\n\n return $this->executeRequest(function () use ($endpoint, $payload) {\n $response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$response ' . PHP_EOL . print_r($response, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$headers ' . PHP_EOL . print_r($response->getHeaders(), true));\n\n return $response->toArray();\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 $deal = $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 return $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n return $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\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 (RateLimitException $e) {\n// throw $e;\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\\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 Illuminate\\Support\\Facades\\Redis;\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\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Reacts to a rate limits (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 $cacheKey = $this->getRateLimitCacheKey();\n\n $cachedRetryAfter = Redis::get($cacheKey);\n if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n (int) $cachedRetryAfter,\n );\n }\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);\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 'policy' => $this->parsePolicy($e),\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 getRateLimitCacheKey(): string\n {\n return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());\n }\n\n public function isHubspotRateLimit(Throwable $e): bool\n {\n if ($e instanceof BadRequest\n || $e instanceof DealApiException\n || $e instanceof ContactApiException\n || $e instanceof CompanyApiException\n || $e instanceof \\GuzzleHttp\\Exception\\RequestException\n ) {\n return (int) $e->getCode() === 429;\n }\n\n return false;\n }\n\n public function parseRetryAfter(Throwable $e): int\n {\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$e ' . PHP_EOL . print_r($e, true));\n return 5;\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 $policy = $this->parsePolicy($e);\n if ($policy === 'TEN_SECONDLY_ROLLING') {\n return 10;\n }\n if ($policy === 'SECONDLY') {\n return 1;\n }\n if ($policy === 'DAILY_LIMIT') {\n return 600;\n }\n\n $this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [\n 'exception_class' => get_class($e),\n ]);\n\n return 10;\n }\n\n public function parsePolicy(Throwable $e): ?string\n {\n if (! method_exists($e, 'getResponseBody')) {\n return null;\n }\n\n $body = $e->getResponseBody();\n if (is_string($body)) {\n $body = json_decode($body, true) ?? [];\n }\n\n if (! is_array($body)) {\n return null;\n }\n\n $policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;\n\n return is_string($policy) ? strtoupper($policy) : null;\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 * Execute a search request against HubSpot CRM objects with rate limiting.\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n * @return array The search response with 'results', 'total', 'paging' keys\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n */\n public function search(string $objectType, array $payload): array\n {\n $endpoint = self::BASE_URL . \"/crm/v3/objects/{$objectType}/search\";\n\n return $this->executeRequest(function () use ($endpoint, $payload) {\n $response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$response ' . PHP_EOL . print_r($response, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$headers ' . PHP_EOL . print_r($response->getHeaders(), true));\n\n return $response->toArray();\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 $deal = $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 return $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n return $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\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 (RateLimitException $e) {\n// throw $e;\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":"AXButton","text":"Show Replace Field","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Search History","depth":3,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"Received 429 from API","depth":4,"on_screen":true,"value":"Received 429 from API","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"New Line","depth":3,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Match Case","depth":3,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Words","depth":3,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Regex","depth":3,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Replace History","depth":3,"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":4,"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":3,"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":3,"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":"AXStaticText","text":"0 results","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Occurrence","depth":4,"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Occurrence","depth":4,"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Filter Search Results","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Open in Window, Multiple Cursors","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Click to highlight","depth":4,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close","depth":4,"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":"[2026-05-11 10:41:16] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"ee9b9096-b1f1-4e2b-ac17-af0003eb00c0\",\"trace_id\":\"8904c61b-d5b4-44cc-91bf-68f0f73222c0\"}\n[2026-05-11 10:41:16] local.INFO: [EmailSchedule] STARTING batch process {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"ee9b9096-b1f1-4e2b-ac17-af0003eb00c0\",\"trace_id\":\"8904c61b-d5b4-44cc-91bf-68f0f73222c0\"}\n[2026-05-11 10:41:17] local.INFO: [EmailSchedule] FINISHED batch process {\"host\":\"docker_lamp_1\",\"processed\":0} {\"correlation_id\":\"ee9b9096-b1f1-4e2b-ac17-af0003eb00c0\",\"trace_id\":\"8904c61b-d5b4-44cc-91bf-68f0f73222c0\"}\n[2026-05-11 10:41:17] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":60.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"ee9b9096-b1f1-4e2b-ac17-af0003eb00c0\",\"trace_id\":\"8904c61b-d5b4-44cc-91bf-68f0f73222c0\"}\n[2026-05-11 10:41:21] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"crm:sync-hubspot-objects\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"13c8d2de-a598-41db-b4ca-9799a4bee946\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:21] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"crm:sync-hubspot-objects\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":60.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"13c8d2de-a598-41db-b4ca-9799a4bee946\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:21] local.INFO: [SyncHubspotObjects] Starting sync {\"team\":\"b2b115eb-93ce-4d1b-929c-173757df8fba\",\"usage\":24894088,\"real_usage\":65011712,\"pid\":62058} {\"correlation_id\":\"81433689-3fcd-4128-8b21-068ce5ee75be\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:21] local.WARNING: [HubSpot] Account not connected for user {\"userId\":\"33e34a7a-1c02-4f04-87ac-22c3a385e6e3\",\"account\":{\"Jiminny\\\\Models\\\\SocialAccount\":{\"id\":306,\"sociable_id\":109,\"provider_user_id\":\"11348452\",\"expires\":1701077403,\"refresh_token_expires\":null,\"provider\":\"hubspot\",\"state\":\"full-refresh\",\"auth_scope\":null,\"retry_after\":null,\"created_at\":\"2020-09-01 16:59:04\",\"updated_at\":\"2023-11-27 09:30:03\"}}} {\"correlation_id\":\"81433689-3fcd-4128-8b21-068ce5ee75be\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:21] local.INFO: [CrmOwnerResolver] Integration owner is not connected, attempting team members {\"crm_provider\":\"hubspot\",\"crm_owner\":109,\"team_id\":29} {\"correlation_id\":\"81433689-3fcd-4128-8b21-068ce5ee75be\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:21] local.INFO: [CrmOwnerResolver] No team members found with active crm connection {\"crm_provider\":\"hubspot\",\"team_id\":29} {\"correlation_id\":\"81433689-3fcd-4128-8b21-068ce5ee75be\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:21] local.INFO: [CrmOwnerResolver] No team member found with active crm connection {\"crm_provider\":\"hubspot\",\"team_id\":29} {\"correlation_id\":\"81433689-3fcd-4128-8b21-068ce5ee75be\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:21] local.INFO: [SyncHubspotObjects] Sync finished {\"team\":\"b2b115eb-93ce-4d1b-929c-173757df8fba\",\"provider\":\"hubspot\",\"status\":\"disconnected\",\"duration_ms\":35.85,\"usage\":24956128,\"real_usage\":65011712,\"pid\":62058,\"reason\":\"Your HubSpot account has become disconnected. Please login to Jiminny to reconnect.\"} {\"correlation_id\":\"81433689-3fcd-4128-8b21-068ce5ee75be\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:21] local.INFO: [SyncHubspotObjects] Starting sync {\"team\":\"abae74b8-bfa8-4383-9a7f-89f4bf2bdbb4\",\"usage\":24914344,\"real_usage\":65011712,\"pid\":62058} {\"correlation_id\":\"d2cb4aff-14c0-474f-89f2-f5c69dafe934\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:21] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"d2cb4aff-14c0-474f-89f2-f5c69dafe934\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:21] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"d2cb4aff-14c0-474f-89f2-f5c69dafe934\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:21] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d2cb4aff-14c0-474f-89f2-f5c69dafe934\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:21] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"d2cb4aff-14c0-474f-89f2-f5c69dafe934\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:21] local.INFO: [HubSpot] Syncing opportunities using strategy: lastModified {\"team\":2} {\"correlation_id\":\"d2cb4aff-14c0-474f-89f2-f5c69dafe934\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:21] local.INFO: [Hubspot] Pagination completed {\"team_id\":2,\"endpoint\":\"https://api.hubapi.com/crm/v3/objects/deals/search\",\"total_requests\":1,\"total_records_fetched\":0,\"total_elapsed_seconds\":0.26,\"average_seconds_per_request\":0.26} {\"correlation_id\":\"d2cb4aff-14c0-474f-89f2-f5c69dafe934\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:21] local.INFO: [HubSpot] Synced opportunities {\"team\":2,\"strategies\":\"lastModified\",\"sync_count\":0,\"total\":0,\"last_synced_id\":null,\"duration_ms\":288.63} {\"correlation_id\":\"d2cb4aff-14c0-474f-89f2-f5c69dafe934\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:21] local.INFO: [SyncHubspotObjects] Sync finished {\"team\":\"abae74b8-bfa8-4383-9a7f-89f4bf2bdbb4\",\"provider\":\"hubspot\",\"status\":\"completed\",\"duration_ms\":324.99,\"usage\":24978696,\"real_usage\":65011712,\"pid\":62058} {\"correlation_id\":\"d2cb4aff-14c0-474f-89f2-f5c69dafe934\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:22] local.INFO: [SyncHubspotObjects] Starting sync {\"team\":\"c6b9d6b0-b48d-4832-a68c-a57d60651888\",\"usage\":24956624,\"real_usage\":65011712,\"pid\":62058} {\"correlation_id\":\"955d9400-2f7f-4170-be31-43bafa1ef3bd\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:22] local.WARNING: [HubSpot] Account not connected for user {\"userId\":\"71e3aac5-fb66-47c5-a236-2d051ae3e319\",\"account\":null} {\"correlation_id\":\"955d9400-2f7f-4170-be31-43bafa1ef3bd\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:22] local.INFO: [CrmOwnerResolver] Integration owner is not connected, attempting team members {\"crm_provider\":\"hubspot\",\"crm_owner\":256,\"team_id\":49} {\"correlation_id\":\"955d9400-2f7f-4170-be31-43bafa1ef3bd\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:22] local.INFO: [CrmOwnerResolver] No team members found with active crm connection {\"crm_provider\":\"hubspot\",\"team_id\":49} {\"correlation_id\":\"955d9400-2f7f-4170-be31-43bafa1ef3bd\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:22] local.INFO: [CrmOwnerResolver] No team member found with active crm connection {\"crm_provider\":\"hubspot\",\"team_id\":49} {\"correlation_id\":\"955d9400-2f7f-4170-be31-43bafa1ef3bd\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:22] local.INFO: [SyncHubspotObjects] Sync finished {\"team\":\"c6b9d6b0-b48d-4832-a68c-a57d60651888\",\"provider\":\"hubspot\",\"status\":\"disconnected\",\"duration_ms\":17.51,\"usage\":24941048,\"real_usage\":65011712,\"pid\":62058,\"reason\":\"Social account for HubSpot cannot be found. Please login to Jiminny to connect.\"} {\"correlation_id\":\"955d9400-2f7f-4170-be31-43bafa1ef3bd\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:22] local.INFO: [SyncHubspotObjects] Starting sync {\"team\":\"b2d49a54-b645-4637-a7ae-a86cfce6e8e4\",\"usage\":24901688,\"real_usage\":65011712,\"pid\":62058} {\"correlation_id\":\"6d0b5717-9160-4c2f-ad12-56e42a2c544a\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:22] local.WARNING: [HubSpot] Account not connected for user {\"userId\":\"2ac0447f-3c8c-4ce0-baeb-b63ddb76fa9b\",\"account\":null} {\"correlation_id\":\"6d0b5717-9160-4c2f-ad12-56e42a2c544a\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:22] local.INFO: [CrmOwnerResolver] Integration owner is not connected, attempting team members {\"crm_provider\":\"hubspot\",\"crm_owner\":130,\"team_id\":42} {\"correlation_id\":\"6d0b5717-9160-4c2f-ad12-56e42a2c544a\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:22] local.INFO: [CrmOwnerResolver] No team members found with active crm connection {\"crm_provider\":\"hubspot\",\"team_id\":42} {\"correlation_id\":\"6d0b5717-9160-4c2f-ad12-56e42a2c544a\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:22] local.INFO: [CrmOwnerResolver] No team member found with active crm connection {\"crm_provider\":\"hubspot\",\"team_id\":42} {\"correlation_id\":\"6d0b5717-9160-4c2f-ad12-56e42a2c544a\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:22] local.INFO: [SyncHubspotObjects] Sync finished {\"team\":\"b2d49a54-b645-4637-a7ae-a86cfce6e8e4\",\"provider\":\"hubspot\",\"status\":\"disconnected\",\"duration_ms\":10.99,\"usage\":24944632,\"real_usage\":65011712,\"pid\":62058,\"reason\":\"Social account for HubSpot cannot be found. Please login to Jiminny to connect.\"} {\"correlation_id\":\"6d0b5717-9160-4c2f-ad12-56e42a2c544a\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:39] local.INFO: [HubSpot Journal Polling] No data {\"correlation_id\":\"1f8f11e6-0cf5-49dd-b8d7-56f51ec27f8e\",\"trace_id\":\"0f08076e-448d-487e-99a0-e73dae5c0ad4\"}\n[2026-05-11 10:41:39] local.WARNING: [HubSpot Journal Polling] Maximum empty results reached, stopping {\"empty_results\":5,\"max_empty_results\":5} {\"correlation_id\":\"1f8f11e6-0cf5-49dd-b8d7-56f51ec27f8e\",\"trace_id\":\"0f08076e-448d-487e-99a0-e73dae5c0ad4\"}\n[2026-05-11 10:41:39] local.WARNING: [HubSpot Journal Polling] Maximum empty results reached, stopping {\"empty_results\":5,\"max_empty_results\":5} {\"correlation_id\":\"1f8f11e6-0cf5-49dd-b8d7-56f51ec27f8e\",\"trace_id\":\"0f08076e-448d-487e-99a0-e73dae5c0ad4\"}\n[2026-05-11 10:41:39] local.INFO: [HubSpot Journal Polling] Service ending {\"runtime_seconds\":57,\"total_cycles\":5,\"files_downloaded\":0,\"empty_files\":0,\"other_portal_skipped\":0,\"total_events\":0,\"events_per_file\":0,\"avg_api_ms\":296.8,\"avg_download_ms\":0.0,\"avg_transform_ms\":0.0,\"avg_process_ms\":0.0,\"peak_memory_mb\":99.73} {\"correlation_id\":\"1f8f11e6-0cf5-49dd-b8d7-56f51ec27f8e\",\"trace_id\":\"0f08076e-448d-487e-99a0-e73dae5c0ad4\"}\n[2026-05-11 10:41:39] local.INFO: [HubSpot Journal Polling] Saved offset to database on cleanup {\"offset\":\"019e15a9-9ea0-7da7-87bc-82592e3ccf0d\"} {\"correlation_id\":\"1f8f11e6-0cf5-49dd-b8d7-56f51ec27f8e\",\"trace_id\":\"0f08076e-448d-487e-99a0-e73dae5c0ad4\"}\n[2026-05-11 10:41:39] local.INFO: [HubSpot Journal Polling] Released polling lock {\"correlation_id\":\"1f8f11e6-0cf5-49dd-b8d7-56f51ec27f8e\",\"trace_id\":\"0f08076e-448d-487e-99a0-e73dae5c0ad4\"}\n[2026-05-11 10:42:07] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"1aa66921-0a71-4d61-8cd7-0663242dca44\",\"trace_id\":\"f6b1d739-39b6-4043-a81e-a1af2c76d401\"}\n[2026-05-11 10:42:07] local.INFO: [ScheduleBotCommand] Number of activities to be captured: 0 {\"correlation_id\":\"1aa66921-0a71-4d61-8cd7-0663242dca44\",\"trace_id\":\"f6b1d739-39b6-4043-a81e-a1af2c76d401\"}\n[2026-05-11 10:42:07] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":60.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"1aa66921-0a71-4d61-8cd7-0663242dca44\",\"trace_id\":\"f6b1d739-39b6-4043-a81e-a1af2c76d401\"}\n[2026-05-11 10:42:10] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"cc420ba5-39bf-4183-95ae-48962a9c124b\",\"trace_id\":\"8daf6081-5c8a-4fae-98d8-e317d8580bc3\"}\n[2026-05-11 10:42:10] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":60.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"cc420ba5-39bf-4183-95ae-48962a9c124b\",\"trace_id\":\"8daf6081-5c8a-4fae-98d8-e317d8580bc3\"}\n[2026-05-11 10:42:13] local.NOTICE: Monitoring start {\"correlation_id\":\"50696062-587c-4c43-8075-7229916ea46f\",\"trace_id\":\"b58f754c-906f-413b-80e6-f89189dbe078\"}\n[2026-05-11 10:42:13] local.NOTICE: Monitoring end {\"correlation_id\":\"50696062-587c-4c43-8075-7229916ea46f\",\"trace_id\":\"b58f754c-906f-413b-80e6-f89189dbe078\"}\n[2026-05-11 10:42:17] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"f70656cd-6de3-4f8b-bb9d-74a941c552c4\",\"trace_id\":\"c780d8e3-2abd-41a3-a9dc-7c2be6aa22d4\"}\n[2026-05-11 10:42:17] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":60.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"f70656cd-6de3-4f8b-bb9d-74a941c552c4\",\"trace_id\":\"c780d8e3-2abd-41a3-a9dc-7c2be6aa22d4\"}\n[2026-05-11 10:42:20] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"b2e73601-1777-4ddb-a152-fe85f4c89afc\",\"trace_id\":\"8bac5054-216a-4605-b13b-9efab8ab5341\"}\n[2026-05-11 10:42:20] local.INFO: [EmailSchedule] STARTING batch process {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"b2e73601-1777-4ddb-a152-fe85f4c89afc\",\"trace_id\":\"8bac5054-216a-4605-b13b-9efab8ab5341\"}\n[2026-05-11 10:42:20] local.INFO: [EmailSchedule] FINISHED batch process {\"host\":\"docker_lamp_1\",\"processed\":0} {\"correlation_id\":\"b2e73601-1777-4ddb-a152-fe85f4c89afc\",\"trace_id\":\"8bac5054-216a-4605-b13b-9efab8ab5341\"}\n[2026-05-11 10:42:20] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":60.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"b2e73601-1777-4ddb-a152-fe85f4c89afc\",\"trace_id\":\"8bac5054-216a-4605-b13b-9efab8ab5341\"}\n[2026-05-11 10:42:31] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"conference:monitor:count\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"175d1973-e4ab-47f7-bb25-14bc3f0a1cce\",\"trace_id\":\"723f475a-f2fc-4235-8163-c2a8ed5777e6\"}\n[2026-05-11 10:42:31] local.INFO: Running conference:monitor:count command for activities in (2026-05-11 10:40:00, 2026-05-11 10:42:00] {\"correlation_id\":\"175d1973-e4ab-47f7-bb25-14bc3f0a1cce\",\"trace_id\":\"723f475a-f2fc-4235-8163-c2a8ed5777e6\"}\n[2026-05-11 10:42:31] local.INFO: [conference:monitor:count] No activities found in (2026-05-11 10:40:00, 2026-05-11 10:42:00] {\"correlation_id\":\"175d1973-e4ab-47f7-bb25-14bc3f0a1cce\",\"trace_id\":\"723f475a-f2fc-4235-8163-c2a8ed5777e6\"}\n[2026-05-11 10:42:31] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"conference:monitor:count\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":60.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"175d1973-e4ab-47f7-bb25-14bc3f0a1cce\",\"trace_id\":\"723f475a-f2fc-4235-8163-c2a8ed5777e6\"}\n[2026-05-11 10:42:36] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:create\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"4243f43b-9d70-4dd0-9c98-3122eb9445ed\",\"trace_id\":\"0c811e1d-ed7e-4aa7-ba81-a14a0cf4b306\"}\n[2026-05-11 10:42:36] local.INFO: [EmailSchedule] STARTING batch create {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"4243f43b-9d70-4dd0-9c98-3122eb9445ed\",\"trace_id\":\"0c811e1d-ed7e-4aa7-ba81-a14a0cf4b306\"}\n[2026-05-11 10:42:36] local.INFO: [EmailSchedule] FINISHED batch create {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"4243f43b-9d70-4dd0-9c98-3122eb9445ed\",\"trace_id\":\"0c811e1d-ed7e-4aa7-ba81-a14a0cf4b306\"}\n[2026-05-11 10:42:36] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:create\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":60.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"4243f43b-9d70-4dd0-9c98-3122eb9445ed\",\"trace_id\":\"0c811e1d-ed7e-4aa7-ba81-a14a0cf4b306\"}\n[2026-05-11 10:42:39] local.INFO: [Jiminny\\Jobs\\Mailbox\\CreateBatches] processed 2 inboxes and created 0 batches {\"userId\":null,\"batchSize\":30,\"maxBatches\":1000} {\"correlation_id\":\"2328b74c-d9ea-43ef-95db-20ea7c6ec9f5\",\"trace_id\":\"0c811e1d-ed7e-4aa7-ba81-a14a0cf4b306\"}\n[2026-05-11 10:42:54] local.INFO: [HubSpot Webhook] Signature validation started {\"method\":\"POST\",\"uri\":\"/webhook/conference/hubspot/events\",\"has_v3_signature\":true,\"has_v1_signature\":true} {\"correlation_id\":\"35391537-e747-4658-94f7-9d3d8c65eab6\",\"trace_id\":\"163e44b3-9f8e-4957-b04d-4937c33d30e6\"}\n[2026-05-11 10:42:54] local.INFO: [HubSpot Webhook] Event received {\"payload\":[{\"eventId\":1531490889,\"subscriptionId\":5683273,\"portalId\":4392066,\"appId\":38482,\"occurredAt\":1778478181931,\"subscriptionType\":\"company.associationChange\",\"attemptNumber\":8,\"changeSource\":\"USER\",\"associationType\":\"COMPANY_TO_DEAL\",\"fromObjectId\":13266161465,\"toObjectId\":60142058500,\"associationRemoved\":false,\"isPrimaryAssociation\":false}],\"event_count\":1} {\"correlation_id\":\"35391537-e747-4658-94f7-9d3d8c65eab6\",\"trace_id\":\"163e44b3-9f8e-4957-b04d-4937c33d30e6\"}\n[2026-05-11 10:42:54] local.INFO: [HubSpot Webhook Job] Processing webhook events {\"event_count\":1,\"local_count\":1,\"forward_count\":1} {\"correlation_id\":\"bb5ee15c-0826-40ef-a4c4-83761c307c7b\",\"trace_id\":\"163e44b3-9f8e-4957-b04d-4937c33d30e6\"}\n[2026-05-11 10:42:55] local.INFO: [Webhook Forwarder] Forwarding to instance {\"url\":\"https://uranus.staging.jiminny.com/internal/webhook-receiver/hubspot\",\"route\":\"internal.webhook-receiver.hubspot\",\"event_count\":1} {\"correlation_id\":\"bb5ee15c-0826-40ef-a4c4-83761c307c7b\",\"trace_id\":\"163e44b3-9f8e-4957-b04d-4937c33d30e6\"}\n[2026-05-11 10:42:55] local.ERROR: [Webhook Forwarder] Failed to forward to instance {\"url\":\"https://uranus.staging.jiminny.com\",\"route\":\"internal.webhook-receiver.hubspot\",\"error\":\"Server error: `POST https://uranus.staging.jiminny.com/internal/webhook-receiver/hubspot` resulted in a `503 Service Temporarily Unavailable` response:\n<html>\n\n<head><title>503 Service Temporarily Unavailable</title></head>\n\n<body>\n\n<center><h1>503 Service Temporarily Una (truncated...)\n\",\"code\":503} {\"correlation_id\":\"bb5ee15c-0826-40ef-a4c4-83761c307c7b\",\"trace_id\":\"163e44b3-9f8e-4957-b04d-4937c33d30e6\"}\n[2026-05-11 10:42:55] local.INFO: [Webhook Forwarder] Forwarding to instance {\"url\":\"https://app.qai.jiminny.com/internal/webhook-receiver/hubspot\",\"route\":\"internal.webhook-receiver.hubspot\",\"event_count\":1} {\"correlation_id\":\"bb5ee15c-0826-40ef-a4c4-83761c307c7b\",\"trace_id\":\"163e44b3-9f8e-4957-b04d-4937c33d30e6\"}\n[2026-05-11 10:42:56] local.INFO: [Webhook Forwarder] Successfully forwarded to instance {\"url\":\"https://app.qai.jiminny.com/internal/webhook-receiver/hubspot\",\"status_code\":202,\"response\":{\"status\":\"accepted\",\"message\":\"Events queued for processing\",\"event_count\":1}} {\"correlation_id\":\"bb5ee15c-0826-40ef-a4c4-83761c307c7b\",\"trace_id\":\"163e44b3-9f8e-4957-b04d-4937c33d30e6\"}\n[2026-05-11 10:42:56] local.INFO: [BatchSyncCollector] Stored webhook in Redis {\"object_type\":\"company\",\"crm_provider_id\":\"13266161465\",\"event_type\":\"association_change\",\"configuration_id\":2,\"batch_key\":\"batch_sync_company:ids:2:all\",\"was_new\":true,\"current_size\":1} {\"correlation_id\":\"bb5ee15c-0826-40ef-a4c4-83761c307c7b\",\"trace_id\":\"163e44b3-9f8e-4957-b04d-4937c33d30e6\"}\n[2026-05-11 10:43:14] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"a2a4326d-0416-4ea7-8eac-370e0fee93fb\",\"trace_id\":\"49cf2405-4c7b-48ce-9aac-074f08037e94\"}\n[2026-05-11 10:43:14] local.INFO: [ScheduleBotCommand] Number of activities to be captured: 0 {\"correlation_id\":\"a2a4326d-0416-4ea7-8eac-370e0fee93fb\",\"trace_id\":\"49cf2405-4c7b-48ce-9aac-074f08037e94\"}\n[2026-05-11 10:43:14] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":60.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"a2a4326d-0416-4ea7-8eac-370e0fee93fb\",\"trace_id\":\"49cf2405-4c7b-48ce-9aac-074f08037e94\"}\n[2026-05-11 10:43:18] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":615092,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"e7be542b-ee51-44b8-a563-a2eac45b3413\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:18] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":615092} {\"correlation_id\":\"e7be542b-ee51-44b8-a563-a2eac45b3413\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:18] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":615092,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"e7be542b-ee51-44b8-a563-a2eac45b3413\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:18] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":615092,\"participants\":[{\"id\":1004102,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":1004103,\"user_id\":89,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"e7be542b-ee51-44b8-a563-a2eac45b3413\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:19] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"e7be542b-ee51-44b8-a563-a2eac45b3413\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:19] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"e7be542b-ee51-44b8-a563-a2eac45b3413\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:19] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"e7be542b-ee51-44b8-a563-a2eac45b3413\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:19] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"e7be542b-ee51-44b8-a563-a2eac45b3413\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:20] local.INFO: [Prospect match] Cache miss, calling the API {\"identifier_type\":\"email\",\"identifier\":\"nikolay.nikolov@jiminny.com\"} {\"correlation_id\":\"e7be542b-ee51-44b8-a563-a2eac45b3413\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:20] local.INFO: [Hubspot] Failed to fetch contact {\"email\":\"nikolay.nikolov@jiminny.com\",\"reason\":\"[404] Client error: `GET https://api.hubapi.com/crm/v3/objects/contacts/nikolay.nikolov%40jiminny.com?properties=email%2Cfirstname%2Clastname%2Ccountry%2Cphone%2Cmobilephone%2Cjobtitle%2Chubspot_owner_id%2Cassociatedcompanyid%2Cphoto&archived=0&idProperty=email` resulted in a `404 Not Found` response\"} {\"correlation_id\":\"e7be542b-ee51-44b8-a563-a2eac45b3413\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:20] local.INFO: [Prospect match] API returned empty result, caching the miss with empty prospect data {\"identifier_type\":\"email\",\"identifier\":\"nikolay.nikolov@jiminny.com\"} {\"correlation_id\":\"e7be542b-ee51-44b8-a563-a2eac45b3413\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:21] local.INFO: [Hubspot] Pagination completed {\"team_id\":2,\"endpoint\":\"https://api.hubapi.com/crm/v3/objects/contact/search\",\"total_requests\":1,\"total_records_fetched\":0,\"total_elapsed_seconds\":0.38,\"average_seconds_per_request\":0.38} {\"correlation_id\":\"e7be542b-ee51-44b8-a563-a2eac45b3413\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:22] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"ab7124d8-7931-41ef-8341-93a3305f65eb\",\"trace_id\":\"4564d24d-bf9e-486b-ac8e-0f69c9b286d8\"}\n[2026-05-11 10:43:22] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":60.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"ab7124d8-7931-41ef-8341-93a3305f65eb\",\"trace_id\":\"4564d24d-bf9e-486b-ac8e-0f69c9b286d8\"}\n[2026-05-11 10:43:22] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"nikolay.nikolov@jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"e7be542b-ee51-44b8-a563-a2eac45b3413\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:22] local.INFO: [Prospect match] Cache miss {\"identifier_type\":\"domain\",\"identifier\":\"jiminny.com\",\"crm\":\"hubspot\"} {\"correlation_id\":\"e7be542b-ee51-44b8-a563-a2eac45b3413\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:22] local.INFO: [Prospect match] Cache miss, calling the API {\"identifier_type\":\"domain\",\"identifier\":\"jiminny.com\"} {\"correlation_id\":\"e7be542b-ee51-44b8-a563-a2eac45b3413\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:23] local.INFO: [HubSpot] importAccount {\"crm_provider_id\":\"749766179\",\"config_id\":2} {\"correlation_id\":\"e7be542b-ee51-44b8-a563-a2eac45b3413\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:23] local.INFO: [HubSpot] CRM Search requested {\"request\":{\"filterGroups\":[{\"filters\":[{\"propertyName\":\"associations.company\",\"operator\":\"EQ\",\"value\":\"749766179\"},{\"propertyName\":\"dealstage\",\"operator\":\"NOT_IN\",\"values\":[\"closedwon\",\"4040964\",\"59247967\"]},{\"propertyName\":\"dealstage\",\"operator\":\"NOT_IN\",\"values\":[\"closedlost\",\"4040965\",\"59247968\"]}]}],\"sorts\":[{\"propertyName\":\"modifieddate\",\"direction\":\"DESCENDING\"}],\"properties\":[\"dealname\",\"amount\",\"hubspot_owner_id\",\"pipeline\",\"dealstage\",\"closedate\",\"deal_currency_code\"],\"limit\":200}} {\"correlation_id\":\"e7be542b-ee51-44b8-a563-a2eac45b3413\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:23] local.INFO: [Hubspot] Pagination completed {\"team_id\":2,\"endpoint\":\"https://api.hubapi.com/crm/v3/objects/deals/search\",\"total_requests\":1,\"total_records_fetched\":10,\"total_elapsed_seconds\":0.3,\"average_seconds_per_request\":0.3} {\"correlation_id\":\"e7be542b-ee51-44b8-a563-a2eac45b3413\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:24] local.INFO: [ EsUpdateProcessManager ] Finished updating entities in ES {\"worker\":\"\",\"peak_memory\":\"99.73 MB\",\"elapsed_seconds\":3.22,\"update_target\":\"activities\",\"should_iterate_again\":false} {\"correlation_id\":\"4ad05333-9afb-492e-9f0f-b2909ac45b32\",\"trace_id\":\"3d8feb24-b173-4158-b0a4-4cf33af85066\"}\n[2026-05-11 10:43:24] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":615092,\"team_id\":2,\"email\":\"nikolay.nikolov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"e7be542b-ee51-44b8-a563-a2eac45b3413\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:24] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":615092,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"e7be542b-ee51-44b8-a563-a2eac45b3413\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:24] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":615092} {\"correlation_id\":\"e7be542b-ee51-44b8-a563-a2eac45b3413\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:24] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":615092,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"e7be542b-ee51-44b8-a563-a2eac45b3413\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:24] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":615092,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"e7be542b-ee51-44b8-a563-a2eac45b3413\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:24] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":614436,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"a34067d1-038f-48bf-af25-c9e09374ce62\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:24] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":614436} {\"correlation_id\":\"a34067d1-038f-48bf-af25-c9e09374ce62\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:24] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":614436,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"a34067d1-038f-48bf-af25-c9e09374ce62\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:24] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":614436,\"participants\":[{\"id\":1002751,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":1002752,\"user_id\":89,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"a34067d1-038f-48bf-af25-c9e09374ce62\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:24] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"a34067d1-038f-48bf-af25-c9e09374ce62\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:24] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"a34067d1-038f-48bf-af25-c9e09374ce62\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:24] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"a34067d1-038f-48bf-af25-c9e09374ce62\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:24] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"a34067d1-038f-48bf-af25-c9e09374ce62\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:24] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"nikolay.nikolov@jiminny.com\"} {\"correlation_id\":\"a34067d1-038f-48bf-af25-c9e09374ce62\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:24] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"nikolay.nikolov@jiminny.com\"} {\"correlation_id\":\"a34067d1-038f-48bf-af25-c9e09374ce62\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:24] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"nikolay.nikolov@jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"a34067d1-038f-48bf-af25-c9e09374ce62\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:24] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":614436,\"team_id\":2,\"email\":\"nikolay.nikolov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"a34067d1-038f-48bf-af25-c9e09374ce62\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:24] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":614436,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"a34067d1-038f-48bf-af25-c9e09374ce62\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:24] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":614436} {\"correlation_id\":\"a34067d1-038f-48bf-af25-c9e09374ce62\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:24] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":614436,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"a34067d1-038f-48bf-af25-c9e09374ce62\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:24] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":614436,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"a34067d1-038f-48bf-af25-c9e09374ce62\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":614382,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"ee88af0b-20d3-4911-a378-bea7fc6dc3f6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":614382} {\"correlation_id\":\"ee88af0b-20d3-4911-a378-bea7fc6dc3f6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":614382,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"ee88af0b-20d3-4911-a378-bea7fc6dc3f6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":614382,\"participants\":[{\"id\":1002632,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":1002633,\"user_id\":89,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"ee88af0b-20d3-4911-a378-bea7fc6dc3f6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"ee88af0b-20d3-4911-a378-bea7fc6dc3f6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"ee88af0b-20d3-4911-a378-bea7fc6dc3f6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"ee88af0b-20d3-4911-a378-bea7fc6dc3f6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"ee88af0b-20d3-4911-a378-bea7fc6dc3f6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"nikolay.nikolov@jiminny.com\"} {\"correlation_id\":\"ee88af0b-20d3-4911-a378-bea7fc6dc3f6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"nikolay.nikolov@jiminny.com\"} {\"correlation_id\":\"ee88af0b-20d3-4911-a378-bea7fc6dc3f6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"nikolay.nikolov@jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"ee88af0b-20d3-4911-a378-bea7fc6dc3f6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":614382,\"team_id\":2,\"email\":\"nikolay.nikolov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"ee88af0b-20d3-4911-a378-bea7fc6dc3f6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":614382,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"ee88af0b-20d3-4911-a378-bea7fc6dc3f6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":614382} {\"correlation_id\":\"ee88af0b-20d3-4911-a378-bea7fc6dc3f6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":614382,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"ee88af0b-20d3-4911-a378-bea7fc6dc3f6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":614382,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"ee88af0b-20d3-4911-a378-bea7fc6dc3f6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":614381,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"535b99f0-d38e-4b9c-bf19-3751ce4e6a1d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":614381} {\"correlation_id\":\"535b99f0-d38e-4b9c-bf19-3751ce4e6a1d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":614381,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"535b99f0-d38e-4b9c-bf19-3751ce4e6a1d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":614381,\"participants\":[{\"id\":1002630,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":1002631,\"user_id\":89,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"535b99f0-d38e-4b9c-bf19-3751ce4e6a1d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"535b99f0-d38e-4b9c-bf19-3751ce4e6a1d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"535b99f0-d38e-4b9c-bf19-3751ce4e6a1d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"535b99f0-d38e-4b9c-bf19-3751ce4e6a1d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"535b99f0-d38e-4b9c-bf19-3751ce4e6a1d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"nikolay.nikolov@jiminny.com\"} {\"correlation_id\":\"535b99f0-d38e-4b9c-bf19-3751ce4e6a1d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"nikolay.nikolov@jiminny.com\"} {\"correlation_id\":\"535b99f0-d38e-4b9c-bf19-3751ce4e6a1d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"nikolay.nikolov@jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"535b99f0-d38e-4b9c-bf19-3751ce4e6a1d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":614381,\"team_id\":2,\"email\":\"nikolay.nikolov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"535b99f0-d38e-4b9c-bf19-3751ce4e6a1d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":614381,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"535b99f0-d38e-4b9c-bf19-3751ce4e6a1d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":614381} {\"correlation_id\":\"535b99f0-d38e-4b9c-bf19-3751ce4e6a1d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":614381,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"535b99f0-d38e-4b9c-bf19-3751ce4e6a1d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":614381,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"535b99f0-d38e-4b9c-bf19-3751ce4e6a1d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":614378,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":6167,\"account_id\":null,\"opportunity_id\":null,\"stage_id\":null}} {\"correlation_id\":\"f934b469-16df-4a4b-b5cc-bf918da87b87\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":614378} {\"correlation_id\":\"f934b469-16df-4a4b-b5cc-bf918da87b87\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":614378,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"f934b469-16df-4a4b-b5cc-bf918da87b87\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":614378,\"participants\":[{\"id\":1002623,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":1002624,\"user_id\":null,\"contact_id\":6167,\"lead_id\":null},{\"id\":1002625,\"user_id\":89,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"f934b469-16df-4a4b-b5cc-bf918da87b87\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"f934b469-16df-4a4b-b5cc-bf918da87b87\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"f934b469-16df-4a4b-b5cc-bf918da87b87\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"f934b469-16df-4a4b-b5cc-bf918da87b87\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"f934b469-16df-4a4b-b5cc-bf918da87b87\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"nikolay.nikolov@jiminny.com\"} {\"correlation_id\":\"f934b469-16df-4a4b-b5cc-bf918da87b87\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"nikolay.nikolov@jiminny.com\"} {\"correlation_id\":\"f934b469-16df-4a4b-b5cc-bf918da87b87\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"nikolay.nikolov@jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"f934b469-16df-4a4b-b5cc-bf918da87b87\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"nmalchev@gmail.com\"} {\"correlation_id\":\"f934b469-16df-4a4b-b5cc-bf918da87b87\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":614378,\"team_id\":2,\"email\":\"nikolay.nikolov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"f934b469-16df-4a4b-b5cc-bf918da87b87\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":614378,\"participants_processed\":3,\"exact_matches\":1,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"f934b469-16df-4a4b-b5cc-bf918da87b87\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":614378} {\"correlation_id\":\"f934b469-16df-4a4b-b5cc-bf918da87b87\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":614378,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"f934b469-16df-4a4b-b5cc-bf918da87b87\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":614378,\"remote_search\":true,\"lead_id\":null,\"contact_id\":6167,\"account_id\":null,\"opportunity_id\":null,\"stage_id\":null} {\"correlation_id\":\"f934b469-16df-4a4b-b5cc-bf918da87b87\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":613840,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36}} {\"correlation_id\":\"b440c986-63bd-445c-adfc-4d111801e63c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613840} {\"correlation_id\":\"b440c986-63bd-445c-adfc-4d111801e63c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613840,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"b440c986-63bd-445c-adfc-4d111801e63c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":613840,\"participants\":[{\"id\":1001764,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null},{\"id\":1001765,\"user_id\":261,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"b440c986-63bd-445c-adfc-4d111801e63c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"b440c986-63bd-445c-adfc-4d111801e63c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"b440c986-63bd-445c-adfc-4d111801e63c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"b440c986-63bd-445c-adfc-4d111801e63c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"b440c986-63bd-445c-adfc-4d111801e63c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: ProspectCache - Searching DB for opportunity by owner {\"account_id\":244,\"contact_id\":4487,\"owner_id\":261} {\"correlation_id\":\"b440c986-63bd-445c-adfc-4d111801e63c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: ProspectCache - Opportunity DB search results {\"account_id\":244,\"contact_id\":4487,\"opportunity_id\":299} {\"correlation_id\":\"b440c986-63bd-445c-adfc-4d111801e63c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"b440c986-63bd-445c-adfc-4d111801e63c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":613840,\"team_id\":2,\"email\":\"adelina.petrova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"b440c986-63bd-445c-adfc-4d111801e63c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":613840,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"b440c986-63bd-445c-adfc-4d111801e63c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613840} {\"correlation_id\":\"b440c986-63bd-445c-adfc-4d111801e63c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613840,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"b440c986-63bd-445c-adfc-4d111801e63c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":613840,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36} {\"correlation_id\":\"b440c986-63bd-445c-adfc-4d111801e63c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":613833,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36}} {\"correlation_id\":\"0360a22a-94dd-43cb-b57b-06bc159a05ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613833} {\"correlation_id\":\"0360a22a-94dd-43cb-b57b-06bc159a05ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613833,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"0360a22a-94dd-43cb-b57b-06bc159a05ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":613833,\"participants\":[{\"id\":1001750,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null},{\"id\":1001751,\"user_id\":261,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"0360a22a-94dd-43cb-b57b-06bc159a05ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"0360a22a-94dd-43cb-b57b-06bc159a05ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"0360a22a-94dd-43cb-b57b-06bc159a05ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"0360a22a-94dd-43cb-b57b-06bc159a05ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"0360a22a-94dd-43cb-b57b-06bc159a05ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"0360a22a-94dd-43cb-b57b-06bc159a05ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":613833,\"team_id\":2,\"email\":\"adelina.petrova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"0360a22a-94dd-43cb-b57b-06bc159a05ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":613833,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"0360a22a-94dd-43cb-b57b-06bc159a05ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613833} {\"correlation_id\":\"0360a22a-94dd-43cb-b57b-06bc159a05ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613833,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"0360a22a-94dd-43cb-b57b-06bc159a05ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":613833,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36} {\"correlation_id\":\"0360a22a-94dd-43cb-b57b-06bc159a05ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":613827,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36}} {\"correlation_id\":\"32fbe130-bf29-4e37-9c5a-bbe81f416a36\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613827} {\"correlation_id\":\"32fbe130-bf29-4e37-9c5a-bbe81f416a36\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613827,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"32fbe130-bf29-4e37-9c5a-bbe81f416a36\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":613827,\"participants\":[{\"id\":1001734,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null},{\"id\":1001735,\"user_id\":261,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"32fbe130-bf29-4e37-9c5a-bbe81f416a36\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"32fbe130-bf29-4e37-9c5a-bbe81f416a36\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"32fbe130-bf29-4e37-9c5a-bbe81f416a36\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"32fbe130-bf29-4e37-9c5a-bbe81f416a36\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"32fbe130-bf29-4e37-9c5a-bbe81f416a36\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"32fbe130-bf29-4e37-9c5a-bbe81f416a36\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":613827,\"team_id\":2,\"email\":\"adelina.petrova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"32fbe130-bf29-4e37-9c5a-bbe81f416a36\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":613827,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"32fbe130-bf29-4e37-9c5a-bbe81f416a36\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613827} {\"correlation_id\":\"32fbe130-bf29-4e37-9c5a-bbe81f416a36\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613827,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"32fbe130-bf29-4e37-9c5a-bbe81f416a36\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":613827,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36} {\"correlation_id\":\"32fbe130-bf29-4e37-9c5a-bbe81f416a36\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":613826,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36}} {\"correlation_id\":\"9b9716ff-ede9-4377-8ee8-b3c9a68b2dfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613826} {\"correlation_id\":\"9b9716ff-ede9-4377-8ee8-b3c9a68b2dfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613826,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"9b9716ff-ede9-4377-8ee8-b3c9a68b2dfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":613826,\"participants\":[{\"id\":1001732,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null},{\"id\":1001733,\"user_id\":261,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"9b9716ff-ede9-4377-8ee8-b3c9a68b2dfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"9b9716ff-ede9-4377-8ee8-b3c9a68b2dfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"9b9716ff-ede9-4377-8ee8-b3c9a68b2dfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"9b9716ff-ede9-4377-8ee8-b3c9a68b2dfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"9b9716ff-ede9-4377-8ee8-b3c9a68b2dfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"9b9716ff-ede9-4377-8ee8-b3c9a68b2dfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":613826,\"team_id\":2,\"email\":\"adelina.petrova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"9b9716ff-ede9-4377-8ee8-b3c9a68b2dfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":613826,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"9b9716ff-ede9-4377-8ee8-b3c9a68b2dfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613826} {\"correlation_id\":\"9b9716ff-ede9-4377-8ee8-b3c9a68b2dfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613826,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"9b9716ff-ede9-4377-8ee8-b3c9a68b2dfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":613826,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36} {\"correlation_id\":\"9b9716ff-ede9-4377-8ee8-b3c9a68b2dfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":613820,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36}} {\"correlation_id\":\"a131075c-6ae7-4912-92e1-adc33c291d1e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613820} {\"correlation_id\":\"a131075c-6ae7-4912-92e1-adc33c291d1e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613820,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"a131075c-6ae7-4912-92e1-adc33c291d1e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":613820,\"participants\":[{\"id\":1001721,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null},{\"id\":1001722,\"user_id\":261,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"a131075c-6ae7-4912-92e1-adc33c291d1e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"a131075c-6ae7-4912-92e1-adc33c291d1e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"a131075c-6ae7-4912-92e1-adc33c291d1e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"a131075c-6ae7-4912-92e1-adc33c291d1e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"a131075c-6ae7-4912-92e1-adc33c291d1e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"a131075c-6ae7-4912-92e1-adc33c291d1e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":613820,\"team_id\":2,\"email\":\"adelina.petrova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"a131075c-6ae7-4912-92e1-adc33c291d1e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":613820,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"a131075c-6ae7-4912-92e1-adc33c291d1e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613820} {\"correlation_id\":\"a131075c-6ae7-4912-92e1-adc33c291d1e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613820,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"a131075c-6ae7-4912-92e1-adc33c291d1e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":613820,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36} {\"correlation_id\":\"a131075c-6ae7-4912-92e1-adc33c291d1e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":613818,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36}} {\"correlation_id\":\"d36e09b8-5b15-40f2-8fa0-0beb3f0ebe69\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613818} {\"correlation_id\":\"d36e09b8-5b15-40f2-8fa0-0beb3f0ebe69\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613818,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"d36e09b8-5b15-40f2-8fa0-0beb3f0ebe69\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":613818,\"participants\":[{\"id\":1001717,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null},{\"id\":1001718,\"user_id\":261,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"d36e09b8-5b15-40f2-8fa0-0beb3f0ebe69\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"d36e09b8-5b15-40f2-8fa0-0beb3f0ebe69\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"d36e09b8-5b15-40f2-8fa0-0beb3f0ebe69\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d36e09b8-5b15-40f2-8fa0-0beb3f0ebe69\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"d36e09b8-5b15-40f2-8fa0-0beb3f0ebe69\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"d36e09b8-5b15-40f2-8fa0-0beb3f0ebe69\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":613818,\"team_id\":2,\"email\":\"adelina.petrova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"d36e09b8-5b15-40f2-8fa0-0beb3f0ebe69\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":613818,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"d36e09b8-5b15-40f2-8fa0-0beb3f0ebe69\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613818} {\"correlation_id\":\"d36e09b8-5b15-40f2-8fa0-0beb3f0ebe69\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613818,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"d36e09b8-5b15-40f2-8fa0-0beb3f0ebe69\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":613818,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36} {\"correlation_id\":\"d36e09b8-5b15-40f2-8fa0-0beb3f0ebe69\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":613812,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36}} {\"correlation_id\":\"9e5ea20b-def9-4ace-8a73-9d08a04cc6d9\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613812} {\"correlation_id\":\"9e5ea20b-def9-4ace-8a73-9d08a04cc6d9\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613812,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"9e5ea20b-def9-4ace-8a73-9d08a04cc6d9\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":613812,\"participants\":[{\"id\":1001705,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null},{\"id\":1001706,\"user_id\":261,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"9e5ea20b-def9-4ace-8a73-9d08a04cc6d9\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"9e5ea20b-def9-4ace-8a73-9d08a04cc6d9\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"9e5ea20b-def9-4ace-8a73-9d08a04cc6d9\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"9e5ea20b-def9-4ace-8a73-9d08a04cc6d9\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"9e5ea20b-def9-4ace-8a73-9d08a04cc6d9\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"9e5ea20b-def9-4ace-8a73-9d08a04cc6d9\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":613812,\"team_id\":2,\"email\":\"adelina.petrova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"9e5ea20b-def9-4ace-8a73-9d08a04cc6d9\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":613812,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"9e5ea20b-def9-4ace-8a73-9d08a04cc6d9\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613812} {\"correlation_id\":\"9e5ea20b-def9-4ace-8a73-9d08a04cc6d9\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613812,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"9e5ea20b-def9-4ace-8a73-9d08a04cc6d9\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":613812,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36} {\"correlation_id\":\"9e5ea20b-def9-4ace-8a73-9d08a04cc6d9\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":613807,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4484,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"be53e876-a319-40dc-a89b-0728d9a47c6e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613807} {\"correlation_id\":\"be53e876-a319-40dc-a89b-0728d9a47c6e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613807,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"be53e876-a319-40dc-a89b-0728d9a47c6e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":613807,\"participants\":[{\"id\":1001690,\"user_id\":253,\"contact_id\":null,\"lead_id\":null},{\"id\":1001691,\"user_id\":null,\"contact_id\":4484,\"lead_id\":null}]} {\"correlation_id\":\"be53e876-a319-40dc-a89b-0728d9a47c6e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"be53e876-a319-40dc-a89b-0728d9a47c6e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"be53e876-a319-40dc-a89b-0728d9a47c6e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"be53e876-a319-40dc-a89b-0728d9a47c6e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"be53e876-a319-40dc-a89b-0728d9a47c6e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":613807,\"team_id\":2,\"email\":\"preslava.ivanova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"be53e876-a319-40dc-a89b-0728d9a47c6e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: ProspectCache - Searching DB for opportunity by owner {\"account_id\":243,\"contact_id\":4484,\"owner_id\":253} {\"correlation_id\":\"be53e876-a319-40dc-a89b-0728d9a47c6e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: ProspectCache - Fallback DB opportunity search {\"account_id\":243,\"contact_id\":4484} {\"correlation_id\":\"be53e876-a319-40dc-a89b-0728d9a47c6e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: ProspectCache - Opportunity DB search results {\"account_id\":243,\"contact_id\":4484,\"opportunity_id\":276} {\"correlation_id\":\"be53e876-a319-40dc-a89b-0728d9a47c6e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"tsvetomir.banovski@gmail.com\"} {\"correlation_id\":\"be53e876-a319-40dc-a89b-0728d9a47c6e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":613807,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"be53e876-a319-40dc-a89b-0728d9a47c6e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613807} {\"correlation_id\":\"be53e876-a319-40dc-a89b-0728d9a47c6e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613807,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"be53e876-a319-40dc-a89b-0728d9a47c6e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":613807,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4484,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"be53e876-a319-40dc-a89b-0728d9a47c6e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":613806,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":350,\"stage_id\":34}} {\"correlation_id\":\"94c53200-4779-41ab-82ea-571c1bcdd8b7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613806} {\"correlation_id\":\"94c53200-4779-41ab-82ea-571c1bcdd8b7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613806,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"94c53200-4779-41ab-82ea-571c1bcdd8b7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":613806,\"participants\":[{\"id\":1001688,\"user_id\":253,\"contact_id\":null,\"lead_id\":null},{\"id\":1001689,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null}]} {\"correlation_id\":\"94c53200-4779-41ab-82ea-571c1bcdd8b7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"94c53200-4779-41ab-82ea-571c1bcdd8b7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"94c53200-4779-41ab-82ea-571c1bcdd8b7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"94c53200-4779-41ab-82ea-571c1bcdd8b7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"94c53200-4779-41ab-82ea-571c1bcdd8b7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":613806,\"team_id\":2,\"email\":\"preslava.ivanova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"94c53200-4779-41ab-82ea-571c1bcdd8b7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: ProspectCache - Searching DB for opportunity by owner {\"account_id\":244,\"contact_id\":4487,\"owner_id\":253} {\"correlation_id\":\"94c53200-4779-41ab-82ea-571c1bcdd8b7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: ProspectCache - Fallback DB opportunity search {\"account_id\":244,\"contact_id\":4487} {\"correlation_id\":\"94c53200-4779-41ab-82ea-571c1bcdd8b7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: ProspectCache - Opportunity DB search results {\"account_id\":244,\"contact_id\":4487,\"opportunity_id\":350} {\"correlation_id\":\"94c53200-4779-41ab-82ea-571c1bcdd8b7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"94c53200-4779-41ab-82ea-571c1bcdd8b7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":613806,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"94c53200-4779-41ab-82ea-571c1bcdd8b7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613806} {\"correlation_id\":\"94c53200-4779-41ab-82ea-571c1bcdd8b7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613806,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"94c53200-4779-41ab-82ea-571c1bcdd8b7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":613806,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":350,\"stage_id\":34} {\"correlation_id\":\"94c53200-4779-41ab-82ea-571c1bcdd8b7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":613805,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":350,\"stage_id\":34}} {\"correlation_id\":\"d1a190ef-9b03-49ff-ad47-50afa53ef345\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613805} {\"correlation_id\":\"d1a190ef-9b03-49ff-ad47-50afa53ef345\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613805,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"d1a190ef-9b03-49ff-ad47-50afa53ef345\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":613805,\"participants\":[{\"id\":1001686,\"user_id\":253,\"contact_id\":null,\"lead_id\":null},{\"id\":1001687,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null}]} {\"correlation_id\":\"d1a190ef-9b03-49ff-ad47-50afa53ef345\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"d1a190ef-9b03-49ff-ad47-50afa53ef345\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"d1a190ef-9b03-49ff-ad47-50afa53ef345\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d1a190ef-9b03-49ff-ad47-50afa53ef345\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"d1a190ef-9b03-49ff-ad47-50afa53ef345\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":613805,\"team_id\":2,\"email\":\"preslava.ivanova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"d1a190ef-9b03-49ff-ad47-50afa53ef345\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"d1a190ef-9b03-49ff-ad47-50afa53ef345\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":613805,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"d1a190ef-9b03-49ff-ad47-50afa53ef345\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613805} {\"correlation_id\":\"d1a190ef-9b03-49ff-ad47-50afa53ef345\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613805,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"d1a190ef-9b03-49ff-ad47-50afa53ef345\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":613805,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":350,\"stage_id\":34} {\"correlation_id\":\"d1a190ef-9b03-49ff-ad47-50afa53ef345\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":613698,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36}} {\"correlation_id\":\"2fa5fec1-54fb-40cc-93d4-145712a9d2fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613698} {\"correlation_id\":\"2fa5fec1-54fb-40cc-93d4-145712a9d2fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613698,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"2fa5fec1-54fb-40cc-93d4-145712a9d2fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":613698,\"participants\":[{\"id\":1001667,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null},{\"id\":1001668,\"user_id\":261,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"2fa5fec1-54fb-40cc-93d4-145712a9d2fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"2fa5fec1-54fb-40cc-93d4-145712a9d2fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"2fa5fec1-54fb-40cc-93d4-145712a9d2fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"2fa5fec1-54fb-40cc-93d4-145712a9d2fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"2fa5fec1-54fb-40cc-93d4-145712a9d2fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"2fa5fec1-54fb-40cc-93d4-145712a9d2fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":613698,\"team_id\":2,\"email\":\"adelina.petrova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"2fa5fec1-54fb-40cc-93d4-145712a9d2fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":613698,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"2fa5fec1-54fb-40cc-93d4-145712a9d2fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613698} {\"correlation_id\":\"2fa5fec1-54fb-40cc-93d4-145712a9d2fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613698,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"2fa5fec1-54fb-40cc-93d4-145712a9d2fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":613698,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36} {\"correlation_id\":\"2fa5fec1-54fb-40cc-93d4-145712a9d2fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":613697,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36}} {\"correlation_id\":\"4da249c2-1056-47f9-869a-53c5fda5b9ef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613697} {\"correlation_id\":\"4da249c2-1056-47f9-869a-53c5fda5b9ef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613697,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"4da249c2-1056-47f9-869a-53c5fda5b9ef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":613697,\"participants\":[{\"id\":1001665,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null},{\"id\":1001666,\"user_id\":261,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"4da249c2-1056-47f9-869a-53c5fda5b9ef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"4da249c2-1056-47f9-869a-53c5fda5b9ef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"4da249c2-1056-47f9-869a-53c5fda5b9ef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4da249c2-1056-47f9-869a-53c5fda5b9ef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"4da249c2-1056-47f9-869a-53c5fda5b9ef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"4da249c2-1056-47f9-869a-53c5fda5b9ef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":613697,\"team_id\":2,\"email\":\"adelina.petrova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"4da249c2-1056-47f9-869a-53c5fda5b9ef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":613697,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"4da249c2-1056-47f9-869a-53c5fda5b9ef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613697} {\"correlation_id\":\"4da249c2-1056-47f9-869a-53c5fda5b9ef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613697,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"4da249c2-1056-47f9-869a-53c5fda5b9ef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":613697,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36} {\"correlation_id\":\"4da249c2-1056-47f9-869a-53c5fda5b9ef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":613696,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36}} {\"correlation_id\":\"8224abca-8e96-4a39-8baa-b4bc4f746e1b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613696} {\"correlation_id\":\"8224abca-8e96-4a39-8baa-b4bc4f746e1b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613696,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"8224abca-8e96-4a39-8baa-b4bc4f746e1b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":613696,\"participants\":[{\"id\":1001663,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null},{\"id\":1001664,\"user_id\":261,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"8224abca-8e96-4a39-8baa-b4bc4f746e1b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"8224abca-8e96-4a39-8baa-b4bc4f746e1b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"8224abca-8e96-4a39-8baa-b4bc4f746e1b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"8224abca-8e96-4a39-8baa-b4bc4f746e1b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"8224abca-8e96-4a39-8baa-b4bc4f746e1b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"8224abca-8e96-4a39-8baa-b4bc4f746e1b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":613696,\"team_id\":2,\"email\":\"adelina.petrova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"8224abca-8e96-4a39-8baa-b4bc4f746e1b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":613696,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"8224abca-8e96-4a39-8baa-b4bc4f746e1b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613696} {\"correlation_id\":\"8224abca-8e96-4a39-8baa-b4bc4f746e1b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613696,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"8224abca-8e96-4a39-8baa-b4bc4f746e1b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":613696,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36} {\"correlation_id\":\"8224abca-8e96-4a39-8baa-b4bc4f746e1b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":613695,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36}} {\"correlation_id\":\"e6c53892-ee7f-42df-ae80-06e4cab7eeea\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613695} {\"correlation_id\":\"e6c53892-ee7f-42df-ae80-06e4cab7eeea\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613695,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"e6c53892-ee7f-42df-ae80-06e4cab7eeea\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":613695,\"participants\":[{\"id\":1001661,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null},{\"id\":1001662,\"user_id\":261,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"e6c53892-ee7f-42df-ae80-06e4cab7eeea\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"e6c53892-ee7f-42df-ae80-06e4cab7eeea\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"e6c53892-ee7f-42df-ae80-06e4cab7eeea\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"e6c53892-ee7f-42df-ae80-06e4cab7eeea\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"e6c53892-ee7f-42df-ae80-06e4cab7eeea\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"e6c53892-ee7f-42df-ae80-06e4cab7eeea\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":613695,\"team_id\":2,\"email\":\"adelina.petrova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"e6c53892-ee7f-42df-ae80-06e4cab7eeea\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":613695,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"e6c53892-ee7f-42df-ae80-06e4cab7eeea\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613695} {\"correlation_id\":\"e6c53892-ee7f-42df-ae80-06e4cab7eeea\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613695,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"e6c53892-ee7f-42df-ae80-06e4cab7eeea\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":613695,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36} {\"correlation_id\":\"e6c53892-ee7f-42df-ae80-06e4cab7eeea\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.NOTICE: Monitoring start {\"correlation_id\":\"2766b9f0-0c60-4d94-9b69-4f2dc977fe59\",\"trace_id\":\"0810b004-42ab-49a4-b3bf-4fbda4a819fe\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":613694,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36}} {\"correlation_id\":\"34f7f8f5-b1b3-4176-bc4a-f96265db81c5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613694} {\"correlation_id\":\"34f7f8f5-b1b3-4176-bc4a-f96265db81c5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613694,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"34f7f8f5-b1b3-4176-bc4a-f96265db81c5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.NOTICE: Monitoring end {\"correlation_id\":\"2766b9f0-0c60-4d94-9b69-4f2dc977fe59\",\"trace_id\":\"0810b004-42ab-49a4-b3bf-4fbda4a819fe\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":613694,\"participants\":[{\"id\":1001659,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null},{\"id\":1001660,\"user_id\":261,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"34f7f8f5-b1b3-4176-bc4a-f96265db81c5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"34f7f8f5-b1b3-4176-bc4a-f96265db81c5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"34f7f8f5-b1b3-4176-bc4a-f96265db81c5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"34f7f8f5-b1b3-4176-bc4a-f96265db81c5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"34f7f8f5-b1b3-4176-bc4a-f96265db81c5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"34f7f8f5-b1b3-4176-bc4a-f96265db81c5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":613694,\"team_id\":2,\"email\":\"adelina.petrova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"34f7f8f5-b1b3-4176-bc4a-f96265db81c5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":613694,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"34f7f8f5-b1b3-4176-bc4a-f96265db81c5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613694} {\"correlation_id\":\"34f7f8f5-b1b3-4176-bc4a-f96265db81c5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613694,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"34f7f8f5-b1b3-4176-bc4a-f96265db81c5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":613694,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36} {\"correlation_id\":\"34f7f8f5-b1b3-4176-bc4a-f96265db81c5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":613157,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":350,\"stage_id\":34}} {\"correlation_id\":\"38fd2794-89d3-4ce9-a9fc-f9f8382e30d5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613157} {\"correlation_id\":\"38fd2794-89d3-4ce9-a9fc-f9f8382e30d5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613157,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"38fd2794-89d3-4ce9-a9fc-f9f8382e30d5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":613157,\"participants\":[{\"id\":1000746,\"user_id\":253,\"contact_id\":null,\"lead_id\":null},{\"id\":1000747,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null}]} {\"correlation_id\":\"38fd2794-89d3-4ce9-a9fc-f9f8382e30d5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"38fd2794-89d3-4ce9-a9fc-f9f8382e30d5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"38fd2794-89d3-4ce9-a9fc-f9f8382e30d5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"38fd2794-89d3-4ce9-a9fc-f9f8382e30d5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"38fd2794-89d3-4ce9-a9fc-f9f8382e30d5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":613157,\"team_id\":2,\"email\":\"preslava.ivanova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"38fd2794-89d3-4ce9-a9fc-f9f8382e30d5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"38fd2794-89d3-4ce9-a9fc-f9f8382e30d5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":613157,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"38fd2794-89d3-4ce9-a9fc-f9f8382e30d5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613157} {\"correlation_id\":\"38fd2794-89d3-4ce9-a9fc-f9f8382e30d5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613157,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"38fd2794-89d3-4ce9-a9fc-f9f8382e30d5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":613157,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":350,\"stage_id\":34} {\"correlation_id\":\"38fd2794-89d3-4ce9-a9fc-f9f8382e30d5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":613156,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":350,\"stage_id\":34}} {\"correlation_id\":\"f33d171a-96c9-465c-bcf2-facecea0a7d6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613156} {\"correlation_id\":\"f33d171a-96c9-465c-bcf2-facecea0a7d6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613156,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"f33d171a-96c9-465c-bcf2-facecea0a7d6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":613156,\"participants\":[{\"id\":1000744,\"user_id\":253,\"contact_id\":null,\"lead_id\":null},{\"id\":1000745,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null}]} {\"correlation_id\":\"f33d171a-96c9-465c-bcf2-facecea0a7d6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"f33d171a-96c9-465c-bcf2-facecea0a7d6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"f33d171a-96c9-465c-bcf2-facecea0a7d6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"f33d171a-96c9-465c-bcf2-facecea0a7d6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"f33d171a-96c9-465c-bcf2-facecea0a7d6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":613156,\"team_id\":2,\"email\":\"preslava.ivanova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"f33d171a-96c9-465c-bcf2-facecea0a7d6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"f33d171a-96c9-465c-bcf2-facecea0a7d6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":613156,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"f33d171a-96c9-465c-bcf2-facecea0a7d6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613156} {\"correlation_id\":\"f33d171a-96c9-465c-bcf2-facecea0a7d6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613156,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"f33d171a-96c9-465c-bcf2-facecea0a7d6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":613156,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":350,\"stage_id\":34} {\"correlation_id\":\"f33d171a-96c9-465c-bcf2-facecea0a7d6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":613155,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":350,\"stage_id\":34}} {\"correlation_id\":\"6907203d-5426-446b-b0ed-cb3a79828876\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613155} {\"correlation_id\":\"6907203d-5426-446b-b0ed-cb3a79828876\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613155,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"6907203d-5426-446b-b0ed-cb3a79828876\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":613155,\"participants\":[{\"id\":1000742,\"user_id\":253,\"contact_id\":null,\"lead_id\":null},{\"id\":1000743,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null}]} {\"correlation_id\":\"6907203d-5426-446b-b0ed-cb3a79828876\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"6907203d-5426-446b-b0ed-cb3a79828876\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"6907203d-5426-446b-b0ed-cb3a79828876\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"6907203d-5426-446b-b0ed-cb3a79828876\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"6907203d-5426-446b-b0ed-cb3a79828876\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":613155,\"team_id\":2,\"email\":\"preslava.ivanova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"6907203d-5426-446b-b0ed-cb3a79828876\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"6907203d-5426-446b-b0ed-cb3a79828876\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":613155,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"6907203d-5426-446b-b0ed-cb3a79828876\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613155} {\"correlation_id\":\"6907203d-5426-446b-b0ed-cb3a79828876\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613155,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"6907203d-5426-446b-b0ed-cb3a79828876\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":613155,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":350,\"stage_id\":34} {\"correlation_id\":\"6907203d-5426-446b-b0ed-cb3a79828876\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":613130,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36}} {\"correlation_id\":\"b39eb37f-ecba-45cf-99d3-dc45ee004976\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613130} {\"correlation_id\":\"b39eb37f-ecba-45cf-99d3-dc45ee004976\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613130,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"b39eb37f-ecba-45cf-99d3-dc45ee004976\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":613130,\"participants\":[{\"id\":1000693,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null},{\"id\":1000694,\"user_id\":261,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"b39eb37f-ecba-45cf-99d3-dc45ee004976\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"b39eb37f-ecba-45cf-99d3-dc45ee004976\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"b39eb37f-ecba-45cf-99d3-dc45ee004976\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"b39eb37f-ecba-45cf-99d3-dc45ee004976\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"b39eb37f-ecba-45cf-99d3-dc45ee004976\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"b39eb37f-ecba-45cf-99d3-dc45ee004976\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":613130,\"team_id\":2,\"email\":\"adelina.petrova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"b39eb37f-ecba-45cf-99d3-dc45ee004976\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":613130,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"b39eb37f-ecba-45cf-99d3-dc45ee004976\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613130} {\"correlation_id\":\"b39eb37f-ecba-45cf-99d3-dc45ee004976\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613130,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"b39eb37f-ecba-45cf-99d3-dc45ee004976\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":613130,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36} {\"correlation_id\":\"b39eb37f-ecba-45cf-99d3-dc45ee004976\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612924,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":165,\"stage_id\":89}} {\"correlation_id\":\"22353f3a-e0aa-43eb-9a06-f618d38746e7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612924} {\"correlation_id\":\"22353f3a-e0aa-43eb-9a06-f618d38746e7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612924,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"22353f3a-e0aa-43eb-9a06-f618d38746e7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612924,\"participants\":[{\"id\":1000290,\"user_id\":19,\"contact_id\":null,\"lead_id\":null},{\"id\":1000291,\"user_id\":null,\"contact_id\":97,\"lead_id\":null}]} {\"correlation_id\":\"22353f3a-e0aa-43eb-9a06-f618d38746e7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"22353f3a-e0aa-43eb-9a06-f618d38746e7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"22353f3a-e0aa-43eb-9a06-f618d38746e7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"22353f3a-e0aa-43eb-9a06-f618d38746e7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"22353f3a-e0aa-43eb-9a06-f618d38746e7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612924,\"team_id\":2,\"email\":\"james.graham@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"22353f3a-e0aa-43eb-9a06-f618d38746e7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: ProspectCache - Searching DB for opportunity by owner {\"account_id\":69,\"contact_id\":97,\"owner_id\":19} {\"correlation_id\":\"22353f3a-e0aa-43eb-9a06-f618d38746e7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: ProspectCache - Opportunity DB search results {\"account_id\":69,\"contact_id\":97,\"opportunity_id\":165} {\"correlation_id\":\"22353f3a-e0aa-43eb-9a06-f618d38746e7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinsoncrusoe@test.com\"} {\"correlation_id\":\"22353f3a-e0aa-43eb-9a06-f618d38746e7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612924,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"22353f3a-e0aa-43eb-9a06-f618d38746e7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612924} {\"correlation_id\":\"22353f3a-e0aa-43eb-9a06-f618d38746e7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612924,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"22353f3a-e0aa-43eb-9a06-f618d38746e7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612924,\"remote_search\":true,\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":165,\"stage_id\":89} {\"correlation_id\":\"22353f3a-e0aa-43eb-9a06-f618d38746e7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612923,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":165,\"stage_id\":89}} {\"correlation_id\":\"602cc3c5-932c-4d3d-9370-35217adac6e0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612923} {\"correlation_id\":\"602cc3c5-932c-4d3d-9370-35217adac6e0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612923,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"602cc3c5-932c-4d3d-9370-35217adac6e0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612923,\"participants\":[{\"id\":1000288,\"user_id\":19,\"contact_id\":null,\"lead_id\":null},{\"id\":1000289,\"user_id\":null,\"contact_id\":97,\"lead_id\":null}]} {\"correlation_id\":\"602cc3c5-932c-4d3d-9370-35217adac6e0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"602cc3c5-932c-4d3d-9370-35217adac6e0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"602cc3c5-932c-4d3d-9370-35217adac6e0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"602cc3c5-932c-4d3d-9370-35217adac6e0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"602cc3c5-932c-4d3d-9370-35217adac6e0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612923,\"team_id\":2,\"email\":\"james.graham@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"602cc3c5-932c-4d3d-9370-35217adac6e0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinsoncrusoe@test.com\"} {\"correlation_id\":\"602cc3c5-932c-4d3d-9370-35217adac6e0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612923,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"602cc3c5-932c-4d3d-9370-35217adac6e0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612923} {\"correlation_id\":\"602cc3c5-932c-4d3d-9370-35217adac6e0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612923,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"602cc3c5-932c-4d3d-9370-35217adac6e0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612923,\"remote_search\":true,\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":165,\"stage_id\":89} {\"correlation_id\":\"602cc3c5-932c-4d3d-9370-35217adac6e0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612922,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":165,\"stage_id\":89}} {\"correlation_id\":\"4272a600-7709-4e0b-9ddd-2bf9a93ee051\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612922} {\"correlation_id\":\"4272a600-7709-4e0b-9ddd-2bf9a93ee051\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612922,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"4272a600-7709-4e0b-9ddd-2bf9a93ee051\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612922,\"participants\":[{\"id\":1000286,\"user_id\":19,\"contact_id\":null,\"lead_id\":null},{\"id\":1000287,\"user_id\":null,\"contact_id\":97,\"lead_id\":null}]} {\"correlation_id\":\"4272a600-7709-4e0b-9ddd-2bf9a93ee051\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"4272a600-7709-4e0b-9ddd-2bf9a93ee051\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"4272a600-7709-4e0b-9ddd-2bf9a93ee051\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4272a600-7709-4e0b-9ddd-2bf9a93ee051\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"4272a600-7709-4e0b-9ddd-2bf9a93ee051\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612922,\"team_id\":2,\"email\":\"james.graham@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"4272a600-7709-4e0b-9ddd-2bf9a93ee051\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinsoncrusoe@test.com\"} {\"correlation_id\":\"4272a600-7709-4e0b-9ddd-2bf9a93ee051\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612922,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"4272a600-7709-4e0b-9ddd-2bf9a93ee051\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612922} {\"correlation_id\":\"4272a600-7709-4e0b-9ddd-2bf9a93ee051\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612922,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"4272a600-7709-4e0b-9ddd-2bf9a93ee051\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612922,\"remote_search\":true,\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":165,\"stage_id\":89} {\"correlation_id\":\"4272a600-7709-4e0b-9ddd-2bf9a93ee051\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612847,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36}} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612847} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612847,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612847,\"participants\":[{\"id\":1000130,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":1000131,\"user_id\":261,\"contact_id\":null,\"lead_id\":null},{\"id\":1000151,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null}]} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [Prospect match] Cache miss, calling the API {\"identifier_type\":\"email\",\"identifier\":\"adelina.petrova@jiminny.com\"} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [Hubspot] Failed to fetch contact {\"email\":\"adelina.petrova@jiminny.com\",\"reason\":\"[404] Client error: `GET https://api.hubapi.com/crm/v3/objects/contacts/adelina.petrova%40jiminny.com?properties=email%2Cfirstname%2Clastname%2Ccountry%2Cphone%2Cmobilephone%2Cjobtitle%2Chubspot_owner_id%2Cassociatedcompanyid%2Cphoto&archived=0&idProperty=email` resulted in a `404 Not Found` response\"} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [Prospect match] API returned empty result, caching the miss with empty prospect data {\"identifier_type\":\"email\",\"identifier\":\"adelina.petrova@jiminny.com\"} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:27] local.INFO: [Hubspot] Pagination completed {\"team_id\":2,\"endpoint\":\"https://api.hubapi.com/crm/v3/objects/contact/search\",\"total_requests\":1,\"total_records_fetched\":0,\"total_elapsed_seconds\":0.22,\"average_seconds_per_request\":0.22} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:27] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"adelina.petrova@jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:27] local.INFO: [Prospect match] Cache miss {\"identifier_type\":\"domain\",\"identifier\":\"jiminny.com\",\"crm\":\"hubspot\"} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:27] local.INFO: [Prospect match] Cache miss, calling the API {\"identifier_type\":\"domain\",\"identifier\":\"jiminny.com\"} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:27] local.INFO: [HubSpot] importAccount {\"crm_provider_id\":\"749766179\",\"config_id\":2} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:27] local.INFO: [HubSpot] CRM Search requested {\"request\":{\"filterGroups\":[{\"filters\":[{\"propertyName\":\"associations.company\",\"operator\":\"EQ\",\"value\":\"749766179\"},{\"propertyName\":\"dealstage\",\"operator\":\"NOT_IN\",\"values\":[\"closedwon\",\"4040964\",\"59247967\"]},{\"propertyName\":\"dealstage\",\"operator\":\"NOT_IN\",\"values\":[\"closedlost\",\"4040965\",\"59247968\"]}]}],\"sorts\":[{\"propertyName\":\"modifieddate\",\"direction\":\"DESCENDING\"}],\"properties\":[\"dealname\",\"amount\",\"hubspot_owner_id\",\"pipeline\",\"dealstage\",\"closedate\",\"deal_currency_code\"],\"limit\":200}} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:27] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"6bdeb8f0-c361-4cb3-9fb1-c6bdf466a73e\",\"trace_id\":\"d050352e-9bf5-497a-99d6-e46661a04a04\"}\n[2026-05-11 10:43:27] local.INFO: [Hubspot] Pagination completed {\"team_id\":2,\"endpoint\":\"https://api.hubapi.com/crm/v3/objects/deals/search\",\"total_requests\":1,\"total_records_fetched\":10,\"total_elapsed_seconds\":0.25,\"average_seconds_per_request\":0.25} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:27] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":60.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"6bdeb8f0-c361-4cb3-9fb1-c6bdf466a73e\",\"trace_id\":\"d050352e-9bf5-497a-99d6-e46661a04a04\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612847,\"team_id\":2,\"email\":\"adelina.petrova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612847,\"participants_processed\":3,\"exact_matches\":1,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612847} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612847,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612847,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612822,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36}} {\"correlation_id\":\"a8970854-ad6a-4751-9cc5-ffe651d20f06\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612822} {\"correlation_id\":\"a8970854-ad6a-4751-9cc5-ffe651d20f06\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612822,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"a8970854-ad6a-4751-9cc5-ffe651d20f06\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612822,\"participants\":[{\"id\":1000080,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null},{\"id\":1000081,\"user_id\":261,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"a8970854-ad6a-4751-9cc5-ffe651d20f06\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"a8970854-ad6a-4751-9cc5-ffe651d20f06\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"a8970854-ad6a-4751-9cc5-ffe651d20f06\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"a8970854-ad6a-4751-9cc5-ffe651d20f06\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"a8970854-ad6a-4751-9cc5-ffe651d20f06\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"a8970854-ad6a-4751-9cc5-ffe651d20f06\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612822,\"team_id\":2,\"email\":\"adelina.petrova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"a8970854-ad6a-4751-9cc5-ffe651d20f06\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612822,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"a8970854-ad6a-4751-9cc5-ffe651d20f06\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612822} {\"correlation_id\":\"a8970854-ad6a-4751-9cc5-ffe651d20f06\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612822,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"a8970854-ad6a-4751-9cc5-ffe651d20f06\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612822,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36} {\"correlation_id\":\"a8970854-ad6a-4751-9cc5-ffe651d20f06\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612819,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36}} {\"correlation_id\":\"16e6bc16-5ab8-49ce-b7da-c292b8c94105\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612819} {\"correlation_id\":\"16e6bc16-5ab8-49ce-b7da-c292b8c94105\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612819,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"16e6bc16-5ab8-49ce-b7da-c292b8c94105\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612819,\"participants\":[{\"id\":1000073,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null},{\"id\":1000074,\"user_id\":261,\"contact_id\":null,\"lead_id\":null},{\"id\":1000075,\"user_id\":null,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"16e6bc16-5ab8-49ce-b7da-c292b8c94105\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"16e6bc16-5ab8-49ce-b7da-c292b8c94105\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"16e6bc16-5ab8-49ce-b7da-c292b8c94105\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"16e6bc16-5ab8-49ce-b7da-c292b8c94105\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"16e6bc16-5ab8-49ce-b7da-c292b8c94105\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"16e6bc16-5ab8-49ce-b7da-c292b8c94105\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612819,\"team_id\":2,\"email\":\"adelina.petrova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"16e6bc16-5ab8-49ce-b7da-c292b8c94105\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"adelina.petrova@jiminny.com\"} {\"correlation_id\":\"16e6bc16-5ab8-49ce-b7da-c292b8c94105\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"adelina.petrova@jiminny.com\"} {\"correlation_id\":\"16e6bc16-5ab8-49ce-b7da-c292b8c94105\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"adelina.petrova@jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"16e6bc16-5ab8-49ce-b7da-c292b8c94105\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612819,\"participants_processed\":3,\"exact_matches\":1,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"16e6bc16-5ab8-49ce-b7da-c292b8c94105\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612819} {\"correlation_id\":\"16e6bc16-5ab8-49ce-b7da-c292b8c94105\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612819,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"16e6bc16-5ab8-49ce-b7da-c292b8c94105\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612819,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36} {\"correlation_id\":\"16e6bc16-5ab8-49ce-b7da-c292b8c94105\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612673,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36}} {\"correlation_id\":\"8711a017-62dd-479f-b84e-123d6e3f40be\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612673} {\"correlation_id\":\"8711a017-62dd-479f-b84e-123d6e3f40be\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612673,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"8711a017-62dd-479f-b84e-123d6e3f40be\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612673,\"participants\":[{\"id\":999993,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null},{\"id\":999994,\"user_id\":261,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"8711a017-62dd-479f-b84e-123d6e3f40be\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"8711a017-62dd-479f-b84e-123d6e3f40be\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"8711a017-62dd-479f-b84e-123d6e3f40be\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"8711a017-62dd-479f-b84e-123d6e3f40be\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"8711a017-62dd-479f-b84e-123d6e3f40be\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"8711a017-62dd-479f-b84e-123d6e3f40be\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612673,\"team_id\":2,\"email\":\"adelina.petrova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"8711a017-62dd-479f-b84e-123d6e3f40be\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612673,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"8711a017-62dd-479f-b84e-123d6e3f40be\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612673} {\"correlation_id\":\"8711a017-62dd-479f-b84e-123d6e3f40be\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612673,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"8711a017-62dd-479f-b84e-123d6e3f40be\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612673,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36} {\"correlation_id\":\"8711a017-62dd-479f-b84e-123d6e3f40be\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612642,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36}} {\"correlation_id\":\"aa7b8829-1739-4f9b-aad1-c6868d8679ca\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612642} {\"correlation_id\":\"aa7b8829-1739-4f9b-aad1-c6868d8679ca\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612642,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"aa7b8829-1739-4f9b-aad1-c6868d8679ca\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612642,\"participants\":[{\"id\":999935,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null},{\"id\":999936,\"user_id\":261,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"aa7b8829-1739-4f9b-aad1-c6868d8679ca\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"aa7b8829-1739-4f9b-aad1-c6868d8679ca\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"aa7b8829-1739-4f9b-aad1-c6868d8679ca\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"aa7b8829-1739-4f9b-aad1-c6868d8679ca\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"aa7b8829-1739-4f9b-aad1-c6868d8679ca\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"aa7b8829-1739-4f9b-aad1-c6868d8679ca\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612642,\"team_id\":2,\"email\":\"adelina.petrova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"aa7b8829-1739-4f9b-aad1-c6868d8679ca\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612642,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"aa7b8829-1739-4f9b-aad1-c6868d8679ca\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612642} {\"correlation_id\":\"aa7b8829-1739-4f9b-aad1-c6868d8679ca\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612642,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"aa7b8829-1739-4f9b-aad1-c6868d8679ca\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612642,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36} {\"correlation_id\":\"aa7b8829-1739-4f9b-aad1-c6868d8679ca\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612598,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"2fef3119-c9e0-421d-b77e-3cf1e26b480a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612598} {\"correlation_id\":\"2fef3119-c9e0-421d-b77e-3cf1e26b480a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612598,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"2fef3119-c9e0-421d-b77e-3cf1e26b480a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612598,\"participants\":[{\"id\":999857,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null},{\"id\":999858,\"user_id\":206,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"2fef3119-c9e0-421d-b77e-3cf1e26b480a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"2fef3119-c9e0-421d-b77e-3cf1e26b480a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"2fef3119-c9e0-421d-b77e-3cf1e26b480a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"2fef3119-c9e0-421d-b77e-3cf1e26b480a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"2fef3119-c9e0-421d-b77e-3cf1e26b480a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: ProspectCache - Searching DB for opportunity by owner {\"account_id\":243,\"contact_id\":4491,\"owner_id\":206} {\"correlation_id\":\"2fef3119-c9e0-421d-b77e-3cf1e26b480a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: ProspectCache - Fallback DB opportunity search {\"account_id\":243,\"contact_id\":4491} {\"correlation_id\":\"2fef3119-c9e0-421d-b77e-3cf1e26b480a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: ProspectCache - Opportunity DB search results {\"account_id\":243,\"contact_id\":4491,\"opportunity_id\":276} {\"correlation_id\":\"2fef3119-c9e0-421d-b77e-3cf1e26b480a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"2fef3119-c9e0-421d-b77e-3cf1e26b480a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612598,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"2fef3119-c9e0-421d-b77e-3cf1e26b480a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612598,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"2fef3119-c9e0-421d-b77e-3cf1e26b480a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612598} {\"correlation_id\":\"2fef3119-c9e0-421d-b77e-3cf1e26b480a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612598,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"2fef3119-c9e0-421d-b77e-3cf1e26b480a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612598,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"2fef3119-c9e0-421d-b77e-3cf1e26b480a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612597,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"f4aa8c68-1048-465c-8ab6-e414220b2b3d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612597} {\"correlation_id\":\"f4aa8c68-1048-465c-8ab6-e414220b2b3d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612597,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"f4aa8c68-1048-465c-8ab6-e414220b2b3d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612597,\"participants\":[{\"id\":999855,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999856,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null}]} {\"correlation_id\":\"f4aa8c68-1048-465c-8ab6-e414220b2b3d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"f4aa8c68-1048-465c-8ab6-e414220b2b3d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"f4aa8c68-1048-465c-8ab6-e414220b2b3d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"f4aa8c68-1048-465c-8ab6-e414220b2b3d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"f4aa8c68-1048-465c-8ab6-e414220b2b3d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612597,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"f4aa8c68-1048-465c-8ab6-e414220b2b3d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"f4aa8c68-1048-465c-8ab6-e414220b2b3d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612597,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"f4aa8c68-1048-465c-8ab6-e414220b2b3d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612597} {\"correlation_id\":\"f4aa8c68-1048-465c-8ab6-e414220b2b3d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612597,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"f4aa8c68-1048-465c-8ab6-e414220b2b3d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612597,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"f4aa8c68-1048-465c-8ab6-e414220b2b3d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612596,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"885d60db-2622-4f69-9826-b15464181dcb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612596} {\"correlation_id\":\"885d60db-2622-4f69-9826-b15464181dcb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612596,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"885d60db-2622-4f69-9826-b15464181dcb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612596,\"participants\":[{\"id\":999853,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999854,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null}]} {\"correlation_id\":\"885d60db-2622-4f69-9826-b15464181dcb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"885d60db-2622-4f69-9826-b15464181dcb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"885d60db-2622-4f69-9826-b15464181dcb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"885d60db-2622-4f69-9826-b15464181dcb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"885d60db-2622-4f69-9826-b15464181dcb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612596,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"885d60db-2622-4f69-9826-b15464181dcb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"885d60db-2622-4f69-9826-b15464181dcb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612596,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"885d60db-2622-4f69-9826-b15464181dcb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612596} {\"correlation_id\":\"885d60db-2622-4f69-9826-b15464181dcb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612596,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"885d60db-2622-4f69-9826-b15464181dcb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612596,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"885d60db-2622-4f69-9826-b15464181dcb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612595,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"0bc8a5e9-778b-4d99-bf0c-d903bd129c55\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612595} {\"correlation_id\":\"0bc8a5e9-778b-4d99-bf0c-d903bd129c55\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612595,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"0bc8a5e9-778b-4d99-bf0c-d903bd129c55\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612595,\"participants\":[{\"id\":999851,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null},{\"id\":999852,\"user_id\":206,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"0bc8a5e9-778b-4d99-bf0c-d903bd129c55\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"0bc8a5e9-778b-4d99-bf0c-d903bd129c55\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"0bc8a5e9-778b-4d99-bf0c-d903bd129c55\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"0bc8a5e9-778b-4d99-bf0c-d903bd129c55\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"0bc8a5e9-778b-4d99-bf0c-d903bd129c55\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"0bc8a5e9-778b-4d99-bf0c-d903bd129c55\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612595,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"0bc8a5e9-778b-4d99-bf0c-d903bd129c55\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612595,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"0bc8a5e9-778b-4d99-bf0c-d903bd129c55\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612595} {\"correlation_id\":\"0bc8a5e9-778b-4d99-bf0c-d903bd129c55\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612595,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"0bc8a5e9-778b-4d99-bf0c-d903bd129c55\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612595,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"0bc8a5e9-778b-4d99-bf0c-d903bd129c55\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612594,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"9710b4b5-e8f1-4fbf-b865-162ea340046c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612594} {\"correlation_id\":\"9710b4b5-e8f1-4fbf-b865-162ea340046c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612594,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"9710b4b5-e8f1-4fbf-b865-162ea340046c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612594,\"participants\":[{\"id\":999849,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999850,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null}]} {\"correlation_id\":\"9710b4b5-e8f1-4fbf-b865-162ea340046c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"9710b4b5-e8f1-4fbf-b865-162ea340046c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"9710b4b5-e8f1-4fbf-b865-162ea340046c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"9710b4b5-e8f1-4fbf-b865-162ea340046c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"9710b4b5-e8f1-4fbf-b865-162ea340046c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612594,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"9710b4b5-e8f1-4fbf-b865-162ea340046c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"9710b4b5-e8f1-4fbf-b865-162ea340046c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612594,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"9710b4b5-e8f1-4fbf-b865-162ea340046c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612594} {\"correlation_id\":\"9710b4b5-e8f1-4fbf-b865-162ea340046c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612594,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"9710b4b5-e8f1-4fbf-b865-162ea340046c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612594,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"9710b4b5-e8f1-4fbf-b865-162ea340046c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612593,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"59a8137b-0518-4c91-b63d-c794df57a8c1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612593} {\"correlation_id\":\"59a8137b-0518-4c91-b63d-c794df57a8c1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612593,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"59a8137b-0518-4c91-b63d-c794df57a8c1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612593,\"participants\":[{\"id\":999847,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999848,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null}]} {\"correlation_id\":\"59a8137b-0518-4c91-b63d-c794df57a8c1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"59a8137b-0518-4c91-b63d-c794df57a8c1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"59a8137b-0518-4c91-b63d-c794df57a8c1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"59a8137b-0518-4c91-b63d-c794df57a8c1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"59a8137b-0518-4c91-b63d-c794df57a8c1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612593,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"59a8137b-0518-4c91-b63d-c794df57a8c1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"59a8137b-0518-4c91-b63d-c794df57a8c1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612593,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"59a8137b-0518-4c91-b63d-c794df57a8c1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612593} {\"correlation_id\":\"59a8137b-0518-4c91-b63d-c794df57a8c1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612593,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"59a8137b-0518-4c91-b63d-c794df57a8c1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612593,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"59a8137b-0518-4c91-b63d-c794df57a8c1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612592,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"e1f2c9b0-a985-4277-b0da-5190fb9b25d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612592} {\"correlation_id\":\"e1f2c9b0-a985-4277-b0da-5190fb9b25d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612592,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"e1f2c9b0-a985-4277-b0da-5190fb9b25d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612592,\"participants\":[{\"id\":999845,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null},{\"id\":999846,\"user_id\":206,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"e1f2c9b0-a985-4277-b0da-5190fb9b25d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"e1f2c9b0-a985-4277-b0da-5190fb9b25d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"e1f2c9b0-a985-4277-b0da-5190fb9b25d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"e1f2c9b0-a985-4277-b0da-5190fb9b25d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"e1f2c9b0-a985-4277-b0da-5190fb9b25d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"e1f2c9b0-a985-4277-b0da-5190fb9b25d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612592,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"e1f2c9b0-a985-4277-b0da-5190fb9b25d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612592,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"e1f2c9b0-a985-4277-b0da-5190fb9b25d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612592} {\"correlation_id\":\"e1f2c9b0-a985-4277-b0da-5190fb9b25d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612592,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"e1f2c9b0-a985-4277-b0da-5190fb9b25d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612592,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"e1f2c9b0-a985-4277-b0da-5190fb9b25d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612591,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"8a5f33ea-490a-41e3-b71a-872c52626583\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612591} {\"correlation_id\":\"8a5f33ea-490a-41e3-b71a-872c52626583\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612591,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"8a5f33ea-490a-41e3-b71a-872c52626583\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612591,\"participants\":[{\"id\":999843,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999844,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null}]} {\"correlation_id\":\"8a5f33ea-490a-41e3-b71a-872c52626583\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"8a5f33ea-490a-41e3-b71a-872c52626583\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"8a5f33ea-490a-41e3-b71a-872c52626583\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"8a5f33ea-490a-41e3-b71a-872c52626583\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"8a5f33ea-490a-41e3-b71a-872c52626583\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612591,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"8a5f33ea-490a-41e3-b71a-872c52626583\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"8a5f33ea-490a-41e3-b71a-872c52626583\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612591,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"8a5f33ea-490a-41e3-b71a-872c52626583\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612591} {\"correlation_id\":\"8a5f33ea-490a-41e3-b71a-872c52626583\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612591,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"8a5f33ea-490a-41e3-b71a-872c52626583\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612591,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"8a5f33ea-490a-41e3-b71a-872c52626583\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612590,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"b34fb574-2229-4ff9-b572-7e2f801295a8\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612590} {\"correlation_id\":\"b34fb574-2229-4ff9-b572-7e2f801295a8\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612590,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"b34fb574-2229-4ff9-b572-7e2f801295a8\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612590,\"participants\":[{\"id\":999841,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999842,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null}]} {\"correlation_id\":\"b34fb574-2229-4ff9-b572-7e2f801295a8\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"b34fb574-2229-4ff9-b572-7e2f801295a8\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"b34fb574-2229-4ff9-b572-7e2f801295a8\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"b34fb574-2229-4ff9-b572-7e2f801295a8\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"b34fb574-2229-4ff9-b572-7e2f801295a8\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612590,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"b34fb574-2229-4ff9-b572-7e2f801295a8\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"b34fb574-2229-4ff9-b572-7e2f801295a8\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612590,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"b34fb574-2229-4ff9-b572-7e2f801295a8\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612590} {\"correlation_id\":\"b34fb574-2229-4ff9-b572-7e2f801295a8\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612590,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"b34fb574-2229-4ff9-b572-7e2f801295a8\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612590,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"b34fb574-2229-4ff9-b572-7e2f801295a8\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612589,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"746b5162-98d0-4e35-96ca-f6756e9b2d39\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612589} {\"correlation_id\":\"746b5162-98d0-4e35-96ca-f6756e9b2d39\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612589,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"746b5162-98d0-4e35-96ca-f6756e9b2d39\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612589,\"participants\":[{\"id\":999839,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999840,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null}]} {\"correlation_id\":\"746b5162-98d0-4e35-96ca-f6756e9b2d39\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"746b5162-98d0-4e35-96ca-f6756e9b2d39\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"746b5162-98d0-4e35-96ca-f6756e9b2d39\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"746b5162-98d0-4e35-96ca-f6756e9b2d39\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"746b5162-98d0-4e35-96ca-f6756e9b2d39\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612589,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"746b5162-98d0-4e35-96ca-f6756e9b2d39\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"746b5162-98d0-4e35-96ca-f6756e9b2d39\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612589,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"746b5162-98d0-4e35-96ca-f6756e9b2d39\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612589} {\"correlation_id\":\"746b5162-98d0-4e35-96ca-f6756e9b2d39\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612589,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"746b5162-98d0-4e35-96ca-f6756e9b2d39\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612589,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"746b5162-98d0-4e35-96ca-f6756e9b2d39\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612588,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"601702ec-6d8b-4aa3-b7e2-780e758b68d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612588} {\"correlation_id\":\"601702ec-6d8b-4aa3-b7e2-780e758b68d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612588,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"601702ec-6d8b-4aa3-b7e2-780e758b68d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612588,\"participants\":[{\"id\":999837,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null},{\"id\":999838,\"user_id\":206,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"601702ec-6d8b-4aa3-b7e2-780e758b68d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"601702ec-6d8b-4aa3-b7e2-780e758b68d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"601702ec-6d8b-4aa3-b7e2-780e758b68d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"601702ec-6d8b-4aa3-b7e2-780e758b68d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"601702ec-6d8b-4aa3-b7e2-780e758b68d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"601702ec-6d8b-4aa3-b7e2-780e758b68d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612588,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"601702ec-6d8b-4aa3-b7e2-780e758b68d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612588,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"601702ec-6d8b-4aa3-b7e2-780e758b68d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612588} {\"correlation_id\":\"601702ec-6d8b-4aa3-b7e2-780e758b68d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612588,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"601702ec-6d8b-4aa3-b7e2-780e758b68d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612588,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"601702ec-6d8b-4aa3-b7e2-780e758b68d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612587,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"75f31c21-1a08-4a7a-beef-a6ee82905eb2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612587} {\"correlation_id\":\"75f31c21-1a08-4a7a-beef-a6ee82905eb2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612587,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"75f31c21-1a08-4a7a-beef-a6ee82905eb2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612587,\"participants\":[{\"id\":999835,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999836,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null}]} {\"correlation_id\":\"75f31c21-1a08-4a7a-beef-a6ee82905eb2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"75f31c21-1a08-4a7a-beef-a6ee82905eb2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"75f31c21-1a08-4a7a-beef-a6ee82905eb2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"75f31c21-1a08-4a7a-beef-a6ee82905eb2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"75f31c21-1a08-4a7a-beef-a6ee82905eb2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612587,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"75f31c21-1a08-4a7a-beef-a6ee82905eb2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"75f31c21-1a08-4a7a-beef-a6ee82905eb2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612587,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"75f31c21-1a08-4a7a-beef-a6ee82905eb2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612587} {\"correlation_id\":\"75f31c21-1a08-4a7a-beef-a6ee82905eb2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612587,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"75f31c21-1a08-4a7a-beef-a6ee82905eb2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612587,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"75f31c21-1a08-4a7a-beef-a6ee82905eb2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612586,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"b79deb62-f26f-4831-8192-2fca40cc0445\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612586} {\"correlation_id\":\"b79deb62-f26f-4831-8192-2fca40cc0445\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612586,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"b79deb62-f26f-4831-8192-2fca40cc0445\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612586,\"participants\":[{\"id\":999833,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null},{\"id\":999834,\"user_id\":206,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"b79deb62-f26f-4831-8192-2fca40cc0445\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"b79deb62-f26f-4831-8192-2fca40cc0445\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"b79deb62-f26f-4831-8192-2fca40cc0445\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"b79deb62-f26f-4831-8192-2fca40cc0445\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"b79deb62-f26f-4831-8192-2fca40cc0445\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"b79deb62-f26f-4831-8192-2fca40cc0445\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612586,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"b79deb62-f26f-4831-8192-2fca40cc0445\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612586,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"b79deb62-f26f-4831-8192-2fca40cc0445\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612586} {\"correlation_id\":\"b79deb62-f26f-4831-8192-2fca40cc0445\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612586,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"b79deb62-f26f-4831-8192-2fca40cc0445\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612586,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"b79deb62-f26f-4831-8192-2fca40cc0445\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612585,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"2aa37843-11f3-4f11-a9ef-3277ddd89ec3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612585} {\"correlation_id\":\"2aa37843-11f3-4f11-a9ef-3277ddd89ec3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612585,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"2aa37843-11f3-4f11-a9ef-3277ddd89ec3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612585,\"participants\":[{\"id\":999831,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999832,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null}]} {\"correlation_id\":\"2aa37843-11f3-4f11-a9ef-3277ddd89ec3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"2aa37843-11f3-4f11-a9ef-3277ddd89ec3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"2aa37843-11f3-4f11-a9ef-3277ddd89ec3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"2aa37843-11f3-4f11-a9ef-3277ddd89ec3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"2aa37843-11f3-4f11-a9ef-3277ddd89ec3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612585,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"2aa37843-11f3-4f11-a9ef-3277ddd89ec3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"2aa37843-11f3-4f11-a9ef-3277ddd89ec3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612585,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"2aa37843-11f3-4f11-a9ef-3277ddd89ec3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612585} {\"correlation_id\":\"2aa37843-11f3-4f11-a9ef-3277ddd89ec3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612585,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"2aa37843-11f3-4f11-a9ef-3277ddd89ec3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612585,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"2aa37843-11f3-4f11-a9ef-3277ddd89ec3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612584,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"07bfbf03-ba7c-47ad-8252-88913ec31644\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612584} {\"correlation_id\":\"07bfbf03-ba7c-47ad-8252-88913ec31644\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612584,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"07bfbf03-ba7c-47ad-8252-88913ec31644\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612584,\"participants\":[{\"id\":999829,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999830,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null}]} {\"correlation_id\":\"07bfbf03-ba7c-47ad-8252-88913ec31644\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"07bfbf03-ba7c-47ad-8252-88913ec31644\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"07bfbf03-ba7c-47ad-8252-88913ec31644\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"07bfbf03-ba7c-47ad-8252-88913ec31644\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"07bfbf03-ba7c-47ad-8252-88913ec31644\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612584,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"07bfbf03-ba7c-47ad-8252-88913ec31644\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"07bfbf03-ba7c-47ad-8252-88913ec31644\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612584,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"07bfbf03-ba7c-47ad-8252-88913ec31644\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612584} {\"correlation_id\":\"07bfbf03-ba7c-47ad-8252-88913ec31644\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612584,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"07bfbf03-ba7c-47ad-8252-88913ec31644\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612584,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"07bfbf03-ba7c-47ad-8252-88913ec31644\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612583,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"64e9fc7f-c17e-4af3-b884-54a2c388e8f0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612583} {\"correlation_id\":\"64e9fc7f-c17e-4af3-b884-54a2c388e8f0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612583,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"64e9fc7f-c17e-4af3-b884-54a2c388e8f0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612583,\"participants\":[{\"id\":999827,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null},{\"id\":999828,\"user_id\":206,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"64e9fc7f-c17e-4af3-b884-54a2c388e8f0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"64e9fc7f-c17e-4af3-b884-54a2c388e8f0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"64e9fc7f-c17e-4af3-b884-54a2c388e8f0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"64e9fc7f-c17e-4af3-b884-54a2c388e8f0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"64e9fc7f-c17e-4af3-b884-54a2c388e8f0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"64e9fc7f-c17e-4af3-b884-54a2c388e8f0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612583,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"64e9fc7f-c17e-4af3-b884-54a2c388e8f0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612583,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"64e9fc7f-c17e-4af3-b884-54a2c388e8f0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612583} {\"correlation_id\":\"64e9fc7f-c17e-4af3-b884-54a2c388e8f0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612583,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"64e9fc7f-c17e-4af3-b884-54a2c388e8f0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612583,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"64e9fc7f-c17e-4af3-b884-54a2c388e8f0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612582,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"23188089-fcae-491d-b35d-c20491564cfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612582} {\"correlation_id\":\"23188089-fcae-491d-b35d-c20491564cfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612582,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"23188089-fcae-491d-b35d-c20491564cfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612582,\"participants\":[{\"id\":999825,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999826,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null}]} {\"correlation_id\":\"23188089-fcae-491d-b35d-c20491564cfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"23188089-fcae-491d-b35d-c20491564cfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"23188089-fcae-491d-b35d-c20491564cfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"23188089-fcae-491d-b35d-c20491564cfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"23188089-fcae-491d-b35d-c20491564cfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612582,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"23188089-fcae-491d-b35d-c20491564cfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"23188089-fcae-491d-b35d-c20491564cfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612582,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"23188089-fcae-491d-b35d-c20491564cfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612582} {\"correlation_id\":\"23188089-fcae-491d-b35d-c20491564cfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612582,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"23188089-fcae-491d-b35d-c20491564cfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612582,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"23188089-fcae-491d-b35d-c20491564cfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"9f9cc209-aee5-4d58-b4ad-1455693e83d6\",\"trace_id\":\"f11e2691-d834-4325-91f7-003e08888b34\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612581,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"91305be5-0942-40ad-86c8-c2b2f018ea2b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612581} {\"correlation_id\":\"91305be5-0942-40ad-86c8-c2b2f018ea2b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612581,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"91305be5-0942-40ad-86c8-c2b2f018ea2b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612581,\"participants\":[{\"id\":999823,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null},{\"id\":999824,\"user_id\":206,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"91305be5-0942-40ad-86c8-c2b2f018ea2b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"91305be5-0942-40ad-86c8-c2b2f018ea2b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"91305be5-0942-40ad-86c8-c2b2f018ea2b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"91305be5-0942-40ad-86c8-c2b2f018ea2b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"91305be5-0942-40ad-86c8-c2b2f018ea2b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"91305be5-0942-40ad-86c8-c2b2f018ea2b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612581,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"91305be5-0942-40ad-86c8-c2b2f018ea2b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612581,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"91305be5-0942-40ad-86c8-c2b2f018ea2b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612581} {\"correlation_id\":\"91305be5-0942-40ad-86c8-c2b2f018ea2b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612581,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"91305be5-0942-40ad-86c8-c2b2f018ea2b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [EmailSchedule] STARTING batch process {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"9f9cc209-aee5-4d58-b4ad-1455693e83d6\",\"trace_id\":\"f11e2691-d834-4325-91f7-003e08888b34\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612581,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"91305be5-0942-40ad-86c8-c2b2f018ea2b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612565,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"be6ed1a4-4495-4ec9-932f-da6698c31cec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [EmailSchedule] FINISHED batch process {\"host\":\"docker_lamp_1\",\"processed\":0} {\"correlation_id\":\"9f9cc209-aee5-4d58-b4ad-1455693e83d6\",\"trace_id\":\"f11e2691-d834-4325-91f7-003e08888b34\"}\n[2026-05-11 10:43:29] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":60.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"9f9cc209-aee5-4d58-b4ad-1455693e83d6\",\"trace_id\":\"f11e2691-d834-4325-91f7-003e08888b34\"}\n[2026-05-11 10:43:29] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612565} {\"correlation_id\":\"be6ed1a4-4495-4ec9-932f-da6698c31cec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612565,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"be6ed1a4-4495-4ec9-932f-da6698c31cec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612565,\"participants\":[{\"id\":999789,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999790,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null}]} {\"correlation_id\":\"be6ed1a4-4495-4ec9-932f-da6698c31cec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"be6ed1a4-4495-4ec9-932f-da6698c31cec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"be6ed1a4-4495-4ec9-932f-da6698c31cec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"be6ed1a4-4495-4ec9-932f-da6698c31cec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"be6ed1a4-4495-4ec9-932f-da6698c31cec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612565,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"be6ed1a4-4495-4ec9-932f-da6698c31cec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"be6ed1a4-4495-4ec9-932f-da6698c31cec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612565,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"be6ed1a4-4495-4ec9-932f-da6698c31cec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612565} {\"correlation_id\":\"be6ed1a4-4495-4ec9-932f-da6698c31cec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612565,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"be6ed1a4-4495-4ec9-932f-da6698c31cec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612565,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"be6ed1a4-4495-4ec9-932f-da6698c31cec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612563,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":350,\"stage_id\":34}} {\"correlation_id\":\"77b39c9c-1e21-4c1d-925b-4acce1b204d3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612563} {\"correlation_id\":\"77b39c9c-1e21-4c1d-925b-4acce1b204d3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612563,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"77b39c9c-1e21-4c1d-925b-4acce1b204d3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612563,\"participants\":[{\"id\":999784,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999785,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null}]} {\"correlation_id\":\"77b39c9c-1e21-4c1d-925b-4acce1b204d3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"77b39c9c-1e21-4c1d-925b-4acce1b204d3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"77b39c9c-1e21-4c1d-925b-4acce1b204d3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"77b39c9c-1e21-4c1d-925b-4acce1b204d3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"77b39c9c-1e21-4c1d-925b-4acce1b204d3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612563,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"77b39c9c-1e21-4c1d-925b-4acce1b204d3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: ProspectCache - Searching DB for opportunity by owner {\"account_id\":244,\"contact_id\":4487,\"owner_id\":206} {\"correlation_id\":\"77b39c9c-1e21-4c1d-925b-4acce1b204d3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: ProspectCache - Fallback DB opportunity search {\"account_id\":244,\"contact_id\":4487} {\"correlation_id\":\"77b39c9c-1e21-4c1d-925b-4acce1b204d3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: ProspectCache - Opportunity DB search results {\"account_id\":244,\"contact_id\":4487,\"opportunity_id\":350} {\"correlation_id\":\"77b39c9c-1e21-4c1d-925b-4acce1b204d3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"77b39c9c-1e21-4c1d-925b-4acce1b204d3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:30] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612563,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"77b39c9c-1e21-4c1d-925b-4acce1b204d3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:30] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612563} {\"correlation_id\":\"77b39c9c-1e21-4c1d-925b-4acce1b204d3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:30] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612563,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"77b39c9c-1e21-4c1d-925b-4acce1b204d3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:30] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612563,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":350,\"stage_id\":34} {\"correlation_id\":\"77b39c9c-1e21-4c1d-925b-4acce1b204d3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:30] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612562,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"485e0be1-a971-4cde-9ead-4276768bdf66\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:30] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612562} {\"correlation_id\":\"485e0be1-a971-4cde-9ead-4276768bdf66\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:30] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612562,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"485e0be1-a971-4cde-9ead-4276768bdf66\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:30] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612562,\"participants\":[{\"id\":999782,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":999783,\"user_id\":206,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"485e0be1-a971-4cde-9ead-4276768bdf66\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:30] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"485e0be1-a971-4cde-9ead-4276768bdf66\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:30] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"485e0be1-a971-4cde-9ead-4276768bdf66\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:30] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"485e0be1-a971-4cde-9ead-4276768bdf66\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:30] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"485e0be1-a971-4cde-9ead-4276768bdf66\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:30] local.INFO: [Prospect match] Cache miss, calling the API {\"identifier_type\":\"email\",\"identifier\":\"447782589921@txt.staging.jiminny.com\"} {\"correlation_id\":\"485e0be1-a971-4cde-9ead-4276768bdf66\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:30] local.INFO: [Hubspot] Failed to fetch contact {\"email\":\"447782589921@txt.staging.jiminny.com\",\"reason\":\"[404] Client error: `GET https://api.hubapi.com/crm/v3/objects/contacts/447782589921%40txt.staging.jiminny.com?properties=email%2Cfirstname%2Clastname%2Ccountry%2Cphone%2Cmobilephone%2Cjobtitle%2Chubspot_owner_id%2Cassociatedcompanyid%2Cphoto&archived=0&idProperty=email` resulted in a `404 Not Found` response\"} {\"correlation_id\":\"485e0be1-a971-4cde-9ead-4276768bdf66\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:30] local.INFO: [Prospect match] API returned empty result, caching the miss with empty prospect data {\"identifier_type\":\"email\",\"identifier\":\"447782589921@txt.staging.jiminny.com\"} {\"correlation_id\":\"485e0be1-a971-4cde-9ead-4276768bdf66\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:30] local.INFO: [Hubspot] Pagination completed {\"team_id\":2,\"endpoint\":\"https://api.hubapi.com/crm/v3/objects/contact/search\",\"total_requests\":1,\"total_records_fetched\":0,\"total_elapsed_seconds\":0.22,\"average_seconds_per_request\":0.22} {\"correlation_id\":\"485e0be1-a971-4cde-9ead-4276768bdf66\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:30] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"447782589921@txt.staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"485e0be1-a971-4cde-9ead-4276768bdf66\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:30] local.INFO: [Prospect match] Cache miss {\"identifier_type\":\"domain\",\"identifier\":\"jiminny.com\",\"crm\":\"hubspot\"} {\"correlation_id\":\"485e0be1-a971-4cde-9ead-4276768bdf66\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:30] local.INFO: [Prospect match] Cache miss, calling the API {\"identifier_type\":\"domain\",\"identifier\":\"jiminny.com\"} {\"correlation_id\":\"485e0be1-a971-4cde-9ead-4276768bdf66\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:31] local.INFO: [HubSpot] importAccount {\"crm_provider_id\":\"749766179\",\"config_id\":2} {\"correlation_id\":\"485e0be1-a971-4cde-9ead-4276768bdf66\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:31] local.INFO: [HubSpot] CRM Search requested {\"request\":{\"filterGroups\":[{\"filters\":[{\"propertyName\":\"associations.company\",\"operator\":\"EQ\",\"value\":\"749766179\"},{\"propertyName\":\"dealstage\",\"operator\":\"NOT_IN\",\"values\":[\"closedwon\",\"4040964\",\"59247967\"]},{\"propertyName\":\"dealstage\",\"operator\":\"NOT_IN\",\"values\":[\"closedlost\",\"4040965\",\"59247968\"]}]}],\"sorts\":[{\"propertyName\":\"modifieddate\",\"direction\":\"DESCENDING\"}],\"properties\":[\"dealname\",\"amount\",\"hubspot_owner_id\",\"pipeline\",\"dealstage\",\"closedate\",\"deal_currency_code\"],\"limit\":200}} {\"correlation_id\":\"485e0be1-a971-4cde-9ead-4276768bdf66\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:31] local.INFO: [Hubspot] Pagination completed {\"team_id\":2,\"endpoint\":\"https://api.hubapi.com/crm/v3/objects/deals/search\",\"total_requests\":1,\"total_records_fetched\":10,\"total_elapsed_seconds\":0.3,\"average_seconds_per_request\":0.3} {\"correlation_id\":\"485e0be1-a971-4cde-9ead-4276768bdf66\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612562,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"485e0be1-a971-4cde-9ead-4276768bdf66\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612562,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"485e0be1-a971-4cde-9ead-4276768bdf66\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612562} {\"correlation_id\":\"485e0be1-a971-4cde-9ead-4276768bdf66\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612562,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"485e0be1-a971-4cde-9ead-4276768bdf66\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612562,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"485e0be1-a971-4cde-9ead-4276768bdf66\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612561,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"9fffce69-30ed-4c66-9ee9-e980e776cd57\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612561} {\"correlation_id\":\"9fffce69-30ed-4c66-9ee9-e980e776cd57\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612561,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"9fffce69-30ed-4c66-9ee9-e980e776cd57\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612561,\"participants\":[{\"id\":999780,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999781,\"user_id\":null,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"9fffce69-30ed-4c66-9ee9-e980e776cd57\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"9fffce69-30ed-4c66-9ee9-e980e776cd57\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"9fffce69-30ed-4c66-9ee9-e980e776cd57\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"9fffce69-30ed-4c66-9ee9-e980e776cd57\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"9fffce69-30ed-4c66-9ee9-e980e776cd57\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612561,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"9fffce69-30ed-4c66-9ee9-e980e776cd57\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [Prospect match] Cache miss, calling the API {\"identifier_type\":\"email\",\"identifier\":\"447700174614.447782589921.OeREojLVnk@txt.staging.jiminny.com\"} {\"correlation_id\":\"9fffce69-30ed-4c66-9ee9-e980e776cd57\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [Hubspot] Failed to fetch contact {\"email\":\"447700174614.447782589921.OeREojLVnk@txt.staging.jiminny.com\",\"reason\":\"[404] Client error: `GET https://api.hubapi.com/crm/v3/objects/contacts/447700174614.447782589921.OeREojLVnk%40txt.staging.jiminny.com?properties=email%2Cfirstname%2Clastname%2Ccountry%2Cphone%2Cmobilephone%2Cjobtitle%2Chubspot_owner_id%2Cassociatedcompanyid%2Cphoto&archived=0&idProperty=email` resulted in a `404 Not Found` response\"} {\"correlation_id\":\"9fffce69-30ed-4c66-9ee9-e980e776cd57\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [Prospect match] API returned empty result, caching the miss with empty prospect data {\"identifier_type\":\"email\",\"identifier\":\"447700174614.447782589921.OeREojLVnk@txt.staging.jiminny.com\"} {\"correlation_id\":\"9fffce69-30ed-4c66-9ee9-e980e776cd57\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"447700174614.447782589921.OeREojLVnk@txt.staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"9fffce69-30ed-4c66-9ee9-e980e776cd57\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612561,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"9fffce69-30ed-4c66-9ee9-e980e776cd57\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612561} {\"correlation_id\":\"9fffce69-30ed-4c66-9ee9-e980e776cd57\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612561,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"9fffce69-30ed-4c66-9ee9-e980e776cd57\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612561,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"9fffce69-30ed-4c66-9ee9-e980e776cd57\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612560,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"6a946453-4ffe-4df1-b91d-f73baf850a7a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612560} {\"correlation_id\":\"6a946453-4ffe-4df1-b91d-f73baf850a7a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612560,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"6a946453-4ffe-4df1-b91d-f73baf850a7a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612560,\"participants\":[{\"id\":999778,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":999779,\"user_id\":206,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"6a946453-4ffe-4df1-b91d-f73baf850a7a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"6a946453-4ffe-4df1-b91d-f73baf850a7a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"6a946453-4ffe-4df1-b91d-f73baf850a7a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"6a946453-4ffe-4df1-b91d-f73baf850a7a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"6a946453-4ffe-4df1-b91d-f73baf850a7a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"447782589921@txt.staging.jiminny.com\"} {\"correlation_id\":\"6a946453-4ffe-4df1-b91d-f73baf850a7a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"447782589921@txt.staging.jiminny.com\"} {\"correlation_id\":\"6a946453-4ffe-4df1-b91d-f73baf850a7a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"447782589921@txt.staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"6a946453-4ffe-4df1-b91d-f73baf850a7a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612560,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"6a946453-4ffe-4df1-b91d-f73baf850a7a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612560,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"6a946453-4ffe-4df1-b91d-f73baf850a7a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612560} {\"correlation_id\":\"6a946453-4ffe-4df1-b91d-f73baf850a7a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612560,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"6a946453-4ffe-4df1-b91d-f73baf850a7a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612560,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"6a946453-4ffe-4df1-b91d-f73baf850a7a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612559,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":5011,\"stage_id\":34}} {\"correlation_id\":\"6231af67-0852-4f81-a268-953b6db0b3aa\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612559} {\"correlation_id\":\"6231af67-0852-4f81-a268-953b6db0b3aa\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612559,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"6231af67-0852-4f81-a268-953b6db0b3aa\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612559,\"participants\":[{\"id\":999776,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999777,\"user_id\":null,\"contact_id\":97,\"lead_id\":null}]} {\"correlation_id\":\"6231af67-0852-4f81-a268-953b6db0b3aa\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"6231af67-0852-4f81-a268-953b6db0b3aa\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"6231af67-0852-4f81-a268-953b6db0b3aa\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"6231af67-0852-4f81-a268-953b6db0b3aa\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"6231af67-0852-4f81-a268-953b6db0b3aa\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612559,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"6231af67-0852-4f81-a268-953b6db0b3aa\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: ProspectCache - Searching DB for opportunity by owner {\"account_id\":69,\"contact_id\":97,\"owner_id\":206} {\"correlation_id\":\"6231af67-0852-4f81-a268-953b6db0b3aa\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: ProspectCache - Fallback DB opportunity search {\"account_id\":69,\"contact_id\":97} {\"correlation_id\":\"6231af67-0852-4f81-a268-953b6db0b3aa\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: ProspectCache - Opportunity DB search results {\"account_id\":69,\"contact_id\":97,\"opportunity_id\":5011} {\"correlation_id\":\"6231af67-0852-4f81-a268-953b6db0b3aa\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinsoncrusoe@test.com\"} {\"correlation_id\":\"6231af67-0852-4f81-a268-953b6db0b3aa\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612559,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"6231af67-0852-4f81-a268-953b6db0b3aa\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612559} {\"correlation_id\":\"6231af67-0852-4f81-a268-953b6db0b3aa\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612559,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"6231af67-0852-4f81-a268-953b6db0b3aa\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612559,\"remote_search\":true,\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":5011,\"stage_id\":34} {\"correlation_id\":\"6231af67-0852-4f81-a268-953b6db0b3aa\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612558,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":5011,\"stage_id\":34}} {\"correlation_id\":\"f8c4783d-18f2-45b6-a1b8-e9d80b788104\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612558} {\"correlation_id\":\"f8c4783d-18f2-45b6-a1b8-e9d80b788104\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612558,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"f8c4783d-18f2-45b6-a1b8-e9d80b788104\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612558,\"participants\":[{\"id\":999774,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999775,\"user_id\":null,\"contact_id\":97,\"lead_id\":null}]} {\"correlation_id\":\"f8c4783d-18f2-45b6-a1b8-e9d80b788104\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"f8c4783d-18f2-45b6-a1b8-e9d80b788104\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"f8c4783d-18f2-45b6-a1b8-e9d80b788104\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"f8c4783d-18f2-45b6-a1b8-e9d80b788104\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"f8c4783d-18f2-45b6-a1b8-e9d80b788104\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612558,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"f8c4783d-18f2-45b6-a1b8-e9d80b788104\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinsoncrusoe@test.com\"} {\"correlation_id\":\"f8c4783d-18f2-45b6-a1b8-e9d80b788104\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612558,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"f8c4783d-18f2-45b6-a1b8-e9d80b788104\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612558} {\"correlation_id\":\"f8c4783d-18f2-45b6-a1b8-e9d80b788104\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612558,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"f8c4783d-18f2-45b6-a1b8-e9d80b788104\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612558,\"remote_search\":true,\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":5011,\"stage_id\":34} {\"correlation_id\":\"f8c4783d-18f2-45b6-a1b8-e9d80b788104\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612557,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":5011,\"stage_id\":34}} {\"correlation_id\":\"d087a635-67a6-4fb4-be19-85f309031ebb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612557} {\"correlation_id\":\"d087a635-67a6-4fb4-be19-85f309031ebb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612557,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"d087a635-67a6-4fb4-be19-85f309031ebb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612557,\"participants\":[{\"id\":999772,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999773,\"user_id\":null,\"contact_id\":97,\"lead_id\":null}]} {\"correlation_id\":\"d087a635-67a6-4fb4-be19-85f309031ebb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"d087a635-67a6-4fb4-be19-85f309031ebb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"d087a635-67a6-4fb4-be19-85f309031ebb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d087a635-67a6-4fb4-be19-85f309031ebb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"d087a635-67a6-4fb4-be19-85f309031ebb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612557,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"d087a635-67a6-4fb4-be19-85f309031ebb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinsoncrusoe@test.com\"} {\"correlation_id\":\"d087a635-67a6-4fb4-be19-85f309031ebb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612557,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"d087a635-67a6-4fb4-be19-85f309031ebb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612557} {\"correlation_id\":\"d087a635-67a6-4fb4-be19-85f309031ebb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612557,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"d087a635-67a6-4fb4-be19-85f309031ebb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612557,\"remote_search\":true,\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":5011,\"stage_id\":34} {\"correlation_id\":\"d087a635-67a6-4fb4-be19-85f309031ebb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612556,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":5011,\"stage_id\":34}} {\"correlation_id\":\"462bb4b0-f644-4f69-b4a8-6030370c2202\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612556} {\"correlation_id\":\"462bb4b0-f644-4f69-b4a8-6030370c2202\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612556,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"462bb4b0-f644-4f69-b4a8-6030370c2202\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612556,\"participants\":[{\"id\":999770,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999771,\"user_id\":null,\"contact_id\":97,\"lead_id\":null}]} {\"correlation_id\":\"462bb4b0-f644-4f69-b4a8-6030370c2202\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"462bb4b0-f644-4f69-b4a8-6030370c2202\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"462bb4b0-f644-4f69-b4a8-6030370c2202\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"462bb4b0-f644-4f69-b4a8-6030370c2202\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"462bb4b0-f644-4f69-b4a8-6030370c2202\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612556,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"462bb4b0-f644-4f69-b4a8-6030370c2202\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinsoncrusoe@test.com\"} {\"correlation_id\":\"462bb4b0-f644-4f69-b4a8-6030370c2202\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612556,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"462bb4b0-f644-4f69-b4a8-6030370c2202\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612556} {\"correlation_id\":\"462bb4b0-f644-4f69-b4a8-6030370c2202\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612556,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"462bb4b0-f644-4f69-b4a8-6030370c2202\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612556,\"remote_search\":true,\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":5011,\"stage_id\":34} {\"correlation_id\":\"462bb4b0-f644-4f69-b4a8-6030370c2202\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612555,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":5011,\"stage_id\":34}} {\"correlation_id\":\"988eea33-83d6-41f7-93a3-9737c43ee6a3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612555} {\"correlation_id\":\"988eea33-83d6-41f7-93a3-9737c43ee6a3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [ EsUpdateProcessManager ] Finished updating entities in ES {\"worker\":\"\",\"peak_memory\":\"99.73 MB\",\"elapsed_seconds\":4.12,\"update_target\":\"activities\",\"should_iterate_again\":false} {\"correlation_id\":\"4ad05333-9afb-492e-9f0f-b2909ac45b32\",\"trace_id\":\"3d8feb24-b173-4158-b0a4-4cf33af85066\"}\n[2026-05-11 10:43:33] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612555,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"988eea33-83d6-41f7-93a3-9737c43ee6a3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612555,\"participants\":[{\"id\":999768,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999769,\"user_id\":null,\"contact_id\":97,\"lead_id\":null}]} {\"correlation_id\":\"988eea33-83d6-41f7-93a3-9737c43ee6a3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"988eea33-83d6-41f7-93a3-9737c43ee6a3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"988eea33-83d6-41f7-93a3-9737c43ee6a3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"988eea33-83d6-41f7-93a3-9737c43ee6a3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"988eea33-83d6-41f7-93a3-9737c43ee6a3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612555,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"988eea33-83d6-41f7-93a3-9737c43ee6a3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinsoncrusoe@test.com\"} {\"correlation_id\":\"988eea33-83d6-41f7-93a3-9737c43ee6a3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612555,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"988eea33-83d6-41f7-93a3-9737c43ee6a3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612555} {\"correlation_id\":\"988eea33-83d6-41f7-93a3-9737c43ee6a3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612555,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"988eea33-83d6-41f7-93a3-9737c43ee6a3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612555,\"remote_search\":true,\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":5011,\"stage_id\":34} {\"correlation_id\":\"988eea33-83d6-41f7-93a3-9737c43ee6a3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612554,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":5011,\"stage_id\":34}} {\"correlation_id\":\"7ff43be6-a32a-412c-bb3d-1bd40aa30f65\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612554} {\"correlation_id\":\"7ff43be6-a32a-412c-bb3d-1bd40aa30f65\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612554,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"7ff43be6-a32a-412c-bb3d-1bd40aa30f65\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612554,\"participants\":[{\"id\":999766,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999767,\"user_id\":null,\"contact_id\":97,\"lead_id\":null}]} {\"correlation_id\":\"7ff43be6-a32a-412c-bb3d-1bd40aa30f65\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"7ff43be6-a32a-412c-bb3d-1bd40aa30f65\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"7ff43be6-a32a-412c-bb3d-1bd40aa30f65\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"7ff43be6-a32a-412c-bb3d-1bd40aa30f65\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"7ff43be6-a32a-412c-bb3d-1bd40aa30f65\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612554,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"7ff43be6-a32a-412c-bb3d-1bd40aa30f65\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinsoncrusoe@test.com\"} {\"correlation_id\":\"7ff43be6-a32a-412c-bb3d-1bd40aa30f65\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612554,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"7ff43be6-a32a-412c-bb3d-1bd40aa30f65\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612554} {\"correlation_id\":\"7ff43be6-a32a-412c-bb3d-1bd40aa30f65\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612554,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"7ff43be6-a32a-412c-bb3d-1bd40aa30f65\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612554,\"remote_search\":true,\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":5011,\"stage_id\":34} {\"correlation_id\":\"7ff43be6-a32a-412c-bb3d-1bd40aa30f65\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612553,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":5011,\"stage_id\":34}} {\"correlation_id\":\"3e6b8bf5-c432-4502-a026-aaf235b3b359\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612553} {\"correlation_id\":\"3e6b8bf5-c432-4502-a026-aaf235b3b359\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612553,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"3e6b8bf5-c432-4502-a026-aaf235b3b359\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612553,\"participants\":[{\"id\":999764,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999765,\"user_id\":null,\"contact_id\":97,\"lead_id\":null}]} {\"correlation_id\":\"3e6b8bf5-c432-4502-a026-aaf235b3b359\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"3e6b8bf5-c432-4502-a026-aaf235b3b359\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"3e6b8bf5-c432-4502-a026-aaf235b3b359\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"3e6b8bf5-c432-4502-a026-aaf235b3b359\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"3e6b8bf5-c432-4502-a026-aaf235b3b359\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612553,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"3e6b8bf5-c432-4502-a026-aaf235b3b359\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinsoncrusoe@test.com\"} {\"correlation_id\":\"3e6b8bf5-c432-4502-a026-aaf235b3b359\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612553,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"3e6b8bf5-c432-4502-a026-aaf235b3b359\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612553} {\"correlation_id\":\"3e6b8bf5-c432-4502-a026-aaf235b3b359\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612553,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"3e6b8bf5-c432-4502-a026-aaf235b3b359\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612553,\"remote_search\":true,\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":5011,\"stage_id\":34} {\"correlation_id\":\"3e6b8bf5-c432-4502-a026-aaf235b3b359\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"calendar:sync\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:34] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612552,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":5011,\"stage_id\":34}} {\"correlation_id\":\"1d70d062-f785-4c2c-81be-d992f1ef25c0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612552} {\"correlation_id\":\"1d70d062-f785-4c2c-81be-d992f1ef25c0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:retry-failed\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"c2dd1ea7-a2bd-44ee-95a4-60f88c2fcef7\",\"trace_id\":\"dcda0937-682f-46f0-bdc7-d2054de63c89\"}\n[2026-05-11 10:43:34] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612552,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"1d70d062-f785-4c2c-81be-d992f1ef25c0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612552,\"participants\":[{\"id\":999762,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999763,\"user_id\":null,\"contact_id\":97,\"lead_id\":null}]} {\"correlation_id\":\"1d70d062-f785-4c2c-81be-d992f1ef25c0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.NOTICE: Calendar sync start {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:34] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"1d70d062-f785-4c2c-81be-d992f1ef25c0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"1d70d062-f785-4c2c-81be-d992f1ef25c0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"1d70d062-f785-4c2c-81be-d992f1ef25c0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"1d70d062-f785-4c2c-81be-d992f1ef25c0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612552,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"1d70d062-f785-4c2c-81be-d992f1ef25c0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinsoncrusoe@test.com\"} {\"correlation_id\":\"1d70d062-f785-4c2c-81be-d992f1ef25c0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612552,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"1d70d062-f785-4c2c-81be-d992f1ef25c0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612552} {\"correlation_id\":\"1d70d062-f785-4c2c-81be-d992f1ef25c0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612552,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"1d70d062-f785-4c2c-81be-d992f1ef25c0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612552,\"remote_search\":true,\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":5011,\"stage_id\":34} {\"correlation_id\":\"1d70d062-f785-4c2c-81be-d992f1ef25c0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:retry-failed\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":60.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"c2dd1ea7-a2bd-44ee-95a4-60f88c2fcef7\",\"trace_id\":\"dcda0937-682f-46f0-bdc7-d2054de63c89\"}\n[2026-05-11 10:43:34] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612551,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":5011,\"stage_id\":34}} {\"correlation_id\":\"35f5694e-2521-4089-a5f1-4adc747a40bf\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612551} {\"correlation_id\":\"35f5694e-2521-4089-a5f1-4adc747a40bf\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612551,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"35f5694e-2521-4089-a5f1-4adc747a40bf\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612551,\"participants\":[{\"id\":999760,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999761,\"user_id\":null,\"contact_id\":97,\"lead_id\":null}]} {\"correlation_id\":\"35f5694e-2521-4089-a5f1-4adc747a40bf\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"35f5694e-2521-4089-a5f1-4adc747a40bf\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"35f5694e-2521-4089-a5f1-4adc747a40bf\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"35f5694e-2521-4089-a5f1-4adc747a40bf\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"35f5694e-2521-4089-a5f1-4adc747a40bf\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612551,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"35f5694e-2521-4089-a5f1-4adc747a40bf\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinsoncrusoe@test.com\"} {\"correlation_id\":\"35f5694e-2521-4089-a5f1-4adc747a40bf\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612551,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"35f5694e-2521-4089-a5f1-4adc747a40bf\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612551} {\"correlation_id\":\"35f5694e-2521-4089-a5f1-4adc747a40bf\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612551,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"35f5694e-2521-4089-a5f1-4adc747a40bf\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612551,\"remote_search\":true,\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":5011,\"stage_id\":34} {\"correlation_id\":\"35f5694e-2521-4089-a5f1-4adc747a40bf\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1393,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:34] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1393,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:34] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612550,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":5011,\"stage_id\":34}} {\"correlation_id\":\"046f384b-a938-4138-a9e6-7df0ff6eb90b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:34] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612550} {\"correlation_id\":\"046f384b-a938-4138-a9e6-7df0ff6eb90b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612550,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"046f384b-a938-4138-a9e6-7df0ff6eb90b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1393,\"provider\":\"google\",\"refreshToken\":\"5aa7e2d96b53201cd16fca5d2e4ef3ad03320971fc064781d18aee3ae7b99fbf\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:34] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612550,\"participants\":[{\"id\":999758,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999759,\"user_id\":null,\"contact_id\":97,\"lead_id\":null}]} {\"correlation_id\":\"046f384b-a938-4138-a9e6-7df0ff6eb90b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"046f384b-a938-4138-a9e6-7df0ff6eb90b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"046f384b-a938-4138-a9e6-7df0ff6eb90b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"046f384b-a938-4138-a9e6-7df0ff6eb90b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"046f384b-a938-4138-a9e6-7df0ff6eb90b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612550,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"046f384b-a938-4138-a9e6-7df0ff6eb90b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinsoncrusoe@test.com\"} {\"correlation_id\":\"046f384b-a938-4138-a9e6-7df0ff6eb90b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612550,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"046f384b-a938-4138-a9e6-7df0ff6eb90b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612550} {\"correlation_id\":\"046f384b-a938-4138-a9e6-7df0ff6eb90b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612550,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"046f384b-a938-4138-a9e6-7df0ff6eb90b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612550,\"remote_search\":true,\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":5011,\"stage_id\":34} {\"correlation_id\":\"046f384b-a938-4138-a9e6-7df0ff6eb90b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612549,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":5011,\"stage_id\":34}} {\"correlation_id\":\"1fe4380c-c6ba-40c4-9bdc-65cf0a157cda\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612549} {\"correlation_id\":\"1fe4380c-c6ba-40c4-9bdc-65cf0a157cda\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612549,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"1fe4380c-c6ba-40c4-9bdc-65cf0a157cda\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612549,\"participants\":[{\"id\":999756,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999757,\"user_id\":null,\"contact_id\":97,\"lead_id\":null}]} {\"correlation_id\":\"1fe4380c-c6ba-40c4-9bdc-65cf0a157cda\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1393,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Account has been deleted\"}} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:34] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:34] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1393,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:34] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"1fe4380c-c6ba-40c4-9bdc-65cf0a157cda\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"1fe4380c-c6ba-40c4-9bdc-65cf0a157cda\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"1fe4380c-c6ba-40c4-9bdc-65cf0a157cda\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"1fe4380c-c6ba-40c4-9bdc-65cf0a157cda\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1387,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:34] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1387,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:34] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:34] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612549,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"1fe4380c-c6ba-40c4-9bdc-65cf0a157cda\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1387,\"provider\":\"google\",\"refreshToken\":\"8157ac6de94842937194009e9c50e459253600f799dacf6a40755ffdbeb5bba6\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:34] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinsoncrusoe@test.com\"} {\"correlation_id\":\"1fe4380c-c6ba-40c4-9bdc-65cf0a157cda\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612549,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"1fe4380c-c6ba-40c4-9bdc-65cf0a157cda\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612549} {\"correlation_id\":\"1fe4380c-c6ba-40c4-9bdc-65cf0a157cda\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612549,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"1fe4380c-c6ba-40c4-9bdc-65cf0a157cda\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612549,\"remote_search\":true,\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":5011,\"stage_id\":34} {\"correlation_id\":\"1fe4380c-c6ba-40c4-9bdc-65cf0a157cda\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612365,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"9f080e5f-9e25-468c-9d04-5cdf618dc528\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612365} {\"correlation_id\":\"9f080e5f-9e25-468c-9d04-5cdf618dc528\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612365,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"9f080e5f-9e25-468c-9d04-5cdf618dc528\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612365,\"participants\":[{\"id\":999563,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null},{\"id\":999564,\"user_id\":206,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"9f080e5f-9e25-468c-9d04-5cdf618dc528\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"9f080e5f-9e25-468c-9d04-5cdf618dc528\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"9f080e5f-9e25-468c-9d04-5cdf618dc528\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"9f080e5f-9e25-468c-9d04-5cdf618dc528\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1387,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Account has been deleted\"}} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"9f080e5f-9e25-468c-9d04-5cdf618dc528\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1387,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"9f080e5f-9e25-468c-9d04-5cdf618dc528\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612365,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"9f080e5f-9e25-468c-9d04-5cdf618dc528\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1348,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612365,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"9f080e5f-9e25-468c-9d04-5cdf618dc528\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1348,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612365} {\"correlation_id\":\"9f080e5f-9e25-468c-9d04-5cdf618dc528\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612365,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"9f080e5f-9e25-468c-9d04-5cdf618dc528\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1348,\"provider\":\"google\",\"refreshToken\":\"9e7d13d3032d0cb1b79d8e95aef01383e8e91eb52ff8ee960c8a0b6b95cd8c73\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612365,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"9f080e5f-9e25-468c-9d04-5cdf618dc528\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612360,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"e1af9830-ccba-44db-a1a0-d9ac3ec0da4d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612360} {\"correlation_id\":\"e1af9830-ccba-44db-a1a0-d9ac3ec0da4d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612360,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"e1af9830-ccba-44db-a1a0-d9ac3ec0da4d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612360,\"participants\":[{\"id\":999552,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null},{\"id\":999553,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999565,\"user_id\":null,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"e1af9830-ccba-44db-a1a0-d9ac3ec0da4d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"e1af9830-ccba-44db-a1a0-d9ac3ec0da4d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"e1af9830-ccba-44db-a1a0-d9ac3ec0da4d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"e1af9830-ccba-44db-a1a0-d9ac3ec0da4d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"e1af9830-ccba-44db-a1a0-d9ac3ec0da4d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"e1af9830-ccba-44db-a1a0-d9ac3ec0da4d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612360,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"e1af9830-ccba-44db-a1a0-d9ac3ec0da4d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1348,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Bad Request\"}} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1348,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1361,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1361,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1361,\"provider\":\"google\",\"refreshToken\":\"6c843da199c2b9907445329304fcc4ec5057a4ee748d8299641764395c08e1fd\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1361,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Account has been deleted\"}} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1361,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [Hubspot] Pagination completed {\"team_id\":2,\"endpoint\":\"https://api.hubapi.com/crm/v3/objects/contact/search\",\"total_requests\":1,\"total_records_fetched\":0,\"total_elapsed_seconds\":0.24,\"average_seconds_per_request\":0.24} {\"correlation_id\":\"e1af9830-ccba-44db-a1a0-d9ac3ec0da4d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612360,\"participants_processed\":3,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"e1af9830-ccba-44db-a1a0-d9ac3ec0da4d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1310,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612360} {\"correlation_id\":\"e1af9830-ccba-44db-a1a0-d9ac3ec0da4d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1310,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612360,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"e1af9830-ccba-44db-a1a0-d9ac3ec0da4d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1310,\"provider\":\"google\",\"refreshToken\":\"e34818922c2830a660813a63f6169a4a9a992ae2cccd7dc8dd7796cfdb470ef1\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612360,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"e1af9830-ccba-44db-a1a0-d9ac3ec0da4d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612340,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"eb3baeb0-9f99-4e3d-98f1-fdc0d69023fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612340} {\"correlation_id\":\"eb3baeb0-9f99-4e3d-98f1-fdc0d69023fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612340,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"eb3baeb0-9f99-4e3d-98f1-fdc0d69023fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612340,\"participants\":[{\"id\":999516,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null},{\"id\":999517,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999518,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":999519,\"user_id\":null,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"eb3baeb0-9f99-4e3d-98f1-fdc0d69023fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"eb3baeb0-9f99-4e3d-98f1-fdc0d69023fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"eb3baeb0-9f99-4e3d-98f1-fdc0d69023fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"eb3baeb0-9f99-4e3d-98f1-fdc0d69023fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"eb3baeb0-9f99-4e3d-98f1-fdc0d69023fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"eb3baeb0-9f99-4e3d-98f1-fdc0d69023fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612340,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"eb3baeb0-9f99-4e3d-98f1-fdc0d69023fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1310,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Bad Request\"}} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1310,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1333,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1333,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1333,\"provider\":\"google\",\"refreshToken\":\"6c902986546d8e8da1dc539b046cdc1d458f519acc972e5b5f1d6a1a295165e0\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [Hubspot] Pagination completed {\"team_id\":2,\"endpoint\":\"https://api.hubapi.com/crm/v3/objects/contact/search\",\"total_requests\":1,\"total_records_fetched\":0,\"total_elapsed_seconds\":0.22,\"average_seconds_per_request\":0.22} {\"correlation_id\":\"eb3baeb0-9f99-4e3d-98f1-fdc0d69023fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612340,\"participants_processed\":4,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"eb3baeb0-9f99-4e3d-98f1-fdc0d69023fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612340} {\"correlation_id\":\"eb3baeb0-9f99-4e3d-98f1-fdc0d69023fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612340,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"eb3baeb0-9f99-4e3d-98f1-fdc0d69023fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612340,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"eb3baeb0-9f99-4e3d-98f1-fdc0d69023fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612339,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"68ce2e50-cf66-4aab-9ce0-ab242e6027c4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612339} {\"correlation_id\":\"68ce2e50-cf66-4aab-9ce0-ab242e6027c4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612339,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"68ce2e50-cf66-4aab-9ce0-ab242e6027c4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612339,\"participants\":[{\"id\":999514,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null},{\"id\":999515,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999540,\"user_id\":null,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"68ce2e50-cf66-4aab-9ce0-ab242e6027c4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"68ce2e50-cf66-4aab-9ce0-ab242e6027c4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"68ce2e50-cf66-4aab-9ce0-ab242e6027c4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"68ce2e50-cf66-4aab-9ce0-ab242e6027c4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"68ce2e50-cf66-4aab-9ce0-ab242e6027c4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"68ce2e50-cf66-4aab-9ce0-ab242e6027c4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612339,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"68ce2e50-cf66-4aab-9ce0-ab242e6027c4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612339,\"participants_processed\":3,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"68ce2e50-cf66-4aab-9ce0-ab242e6027c4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1333,\"provider\":\"google\",\"responseBody\":{\"error\":\"unauthorized_client\",\"error_description\":\"Unauthorized\"}} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612339} {\"correlation_id\":\"68ce2e50-cf66-4aab-9ce0-ab242e6027c4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1333,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612339,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"68ce2e50-cf66-4aab-9ce0-ab242e6027c4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612339,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"68ce2e50-cf66-4aab-9ce0-ab242e6027c4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1368,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1368,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1368,\"provider\":\"google\",\"refreshToken\":\"d2f128898ff8543bd16b69cfae37896ab85119b0f5ed2b431d739593bb600333\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612336,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"ffc0ea4a-744d-49a4-a9ad-736fbda03384\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612336} {\"correlation_id\":\"ffc0ea4a-744d-49a4-a9ad-736fbda03384\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612336,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"ffc0ea4a-744d-49a4-a9ad-736fbda03384\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612336,\"participants\":[{\"id\":999508,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null},{\"id\":999509,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999512,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":999513,\"user_id\":null,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"ffc0ea4a-744d-49a4-a9ad-736fbda03384\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"ffc0ea4a-744d-49a4-a9ad-736fbda03384\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"ffc0ea4a-744d-49a4-a9ad-736fbda03384\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"ffc0ea4a-744d-49a4-a9ad-736fbda03384\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"ffc0ea4a-744d-49a4-a9ad-736fbda03384\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"ffc0ea4a-744d-49a4-a9ad-736fbda03384\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612336,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"ffc0ea4a-744d-49a4-a9ad-736fbda03384\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612336,\"participants_processed\":4,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"ffc0ea4a-744d-49a4-a9ad-736fbda03384\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612336} {\"correlation_id\":\"ffc0ea4a-744d-49a4-a9ad-736fbda03384\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612336,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"ffc0ea4a-744d-49a4-a9ad-736fbda03384\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612336,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"ffc0ea4a-744d-49a4-a9ad-736fbda03384\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612183,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36}} {\"correlation_id\":\"48880be8-e032-41b2-948e-c4ec99247548\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612183} {\"correlation_id\":\"48880be8-e032-41b2-948e-c4ec99247548\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612183,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"48880be8-e032-41b2-948e-c4ec99247548\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612183,\"participants\":[{\"id\":999227,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null},{\"id\":999228,\"user_id\":261,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"48880be8-e032-41b2-948e-c4ec99247548\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"48880be8-e032-41b2-948e-c4ec99247548\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"48880be8-e032-41b2-948e-c4ec99247548\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"48880be8-e032-41b2-948e-c4ec99247548\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"48880be8-e032-41b2-948e-c4ec99247548\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"48880be8-e032-41b2-948e-c4ec99247548\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612183,\"team_id\":2,\"email\":\"adelina.petrova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"48880be8-e032-41b2-948e-c4ec99247548\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612183,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"48880be8-e032-41b2-948e-c4ec99247548\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612183} {\"correlation_id\":\"48880be8-e032-41b2-948e-c4ec99247548\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612183,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"48880be8-e032-41b2-948e-c4ec99247548\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612183,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36} {\"correlation_id\":\"48880be8-e032-41b2-948e-c4ec99247548\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612182,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36}} {\"correlation_id\":\"3858f13d-426c-474e-98cf-11576b98e5dd\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612182} {\"correlation_id\":\"3858f13d-426c-474e-98cf-11576b98e5dd\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612182,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"3858f13d-426c-474e-98cf-11576b98e5dd\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612182,\"participants\":[{\"id\":999225,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null},{\"id\":999226,\"user_id\":261,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"3858f13d-426c-474e-98cf-11576b98e5dd\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"3858f13d-426c-474e-98cf-11576b98e5dd\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"3858f13d-426c-474e-98cf-11576b98e5dd\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"3858f13d-426c-474e-98cf-11576b98e5dd\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"3858f13d-426c-474e-98cf-11576b98e5dd\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"3858f13d-426c-474e-98cf-11576b98e5dd\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612182,\"team_id\":2,\"email\":\"adelina.petrova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"3858f13d-426c-474e-98cf-11576b98e5dd\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612182,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"3858f13d-426c-474e-98cf-11576b98e5dd\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612182} {\"correlation_id\":\"3858f13d-426c-474e-98cf-11576b98e5dd\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612182,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"3858f13d-426c-474e-98cf-11576b98e5dd\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612182,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36} {\"correlation_id\":\"3858f13d-426c-474e-98cf-11576b98e5dd\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612181,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36}} {\"correlation_id\":\"33fd8677-950a-4069-9642-511743b8b619\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612181} {\"correlation_id\":\"33fd8677-950a-4069-9642-511743b8b619\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612181,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"33fd8677-950a-4069-9642-511743b8b619\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612181,\"participants\":[{\"id\":999223,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null},{\"id\":999224,\"user_id\":261,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"33fd8677-950a-4069-9642-511743b8b619\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"33fd8677-950a-4069-9642-511743b8b619\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"33fd8677-950a-4069-9642-511743b8b619\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"33fd8677-950a-4069-9642-511743b8b619\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"33fd8677-950a-4069-9642-511743b8b619\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"33fd8677-950a-4069-9642-511743b8b619\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1368,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Bad Request\"}} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612181,\"team_id\":2,\"email\":\"adelina.petrova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"33fd8677-950a-4069-9642-511743b8b619\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1368,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612181,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"33fd8677-950a-4069-9642-511743b8b619\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612181} {\"correlation_id\":\"33fd8677-950a-4069-9642-511743b8b619\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612181,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"33fd8677-950a-4069-9642-511743b8b619\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612181,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36} {\"correlation_id\":\"33fd8677-950a-4069-9642-511743b8b619\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1365,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1365,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1365,\"provider\":\"google\",\"refreshToken\":\"7676e4a9afcd082b413248ab5ec6e487021fec6a9bdf315860a59cefad9caad8\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612180,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36}} {\"correlation_id\":\"dc184f1d-d7e2-4bc1-92a0-c572af69ba86\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612180} {\"correlation_id\":\"dc184f1d-d7e2-4bc1-92a0-c572af69ba86\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612180,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"dc184f1d-d7e2-4bc1-92a0-c572af69ba86\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612180,\"participants\":[{\"id\":999221,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null},{\"id\":999222,\"user_id\":261,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"dc184f1d-d7e2-4bc1-92a0-c572af69ba86\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"dc184f1d-d7e2-4bc1-92a0-c572af69ba86\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"dc184f1d-d7e2-4bc1-92a0-c572af69ba86\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"dc184f1d-d7e2-4bc1-92a0-c572af69ba86\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"dc184f1d-d7e2-4bc1-92a0-c572af69ba86\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"dc184f1d-d7e2-4bc1-92a0-c572af69ba86\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612180,\"team_id\":2,\"email\":\"adelina.petrova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"dc184f1d-d7e2-4bc1-92a0-c572af69ba86\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612180,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"dc184f1d-d7e2-4bc1-92a0-c572af69ba86\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612180} {\"correlation_id\":\"dc184f1d-d7e2-4bc1-92a0-c572af69ba86\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612180,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"dc184f1d-d7e2-4bc1-92a0-c572af69ba86\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612180,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36} {\"correlation_id\":\"dc184f1d-d7e2-4bc1-92a0-c572af69ba86\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":611455,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"8f14fe33-00cc-4910-996e-efa541fc5bb7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":611455} {\"correlation_id\":\"8f14fe33-00cc-4910-996e-efa541fc5bb7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":611455,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"8f14fe33-00cc-4910-996e-efa541fc5bb7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":611455,\"participants\":[{\"id\":997961,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":997962,\"user_id\":1460,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"8f14fe33-00cc-4910-996e-efa541fc5bb7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"8f14fe33-00cc-4910-996e-efa541fc5bb7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"8f14fe33-00cc-4910-996e-efa541fc5bb7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"8f14fe33-00cc-4910-996e-efa541fc5bb7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"8f14fe33-00cc-4910-996e-efa541fc5bb7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [Prospect match] Cache miss, calling the API {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"8f14fe33-00cc-4910-996e-efa541fc5bb7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1365,\"provider\":\"google\",\"responseBody\":{\"error\":\"unauthorized_client\",\"error_description\":\"Unauthorized\"}} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1365,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1364,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1364,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1364,\"provider\":\"google\",\"refreshToken\":\"dd5882ebce76e645292ce33ae74238abbb77c0a4ecc6a2bfe723cad82e72ba8e\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.INFO: [Hubspot] Failed to fetch contact {\"email\":\"support@staging.jiminny.com\",\"reason\":\"[404] Client error: `GET https://api.hubapi.com/crm/v3/objects/contacts/support%40staging.jiminny.com?properties=email%2Cfirstname%2Clastname%2Ccountry%2Cphone%2Cmobilephone%2Cjobtitle%2Chubspot_owner_id%2Cassociatedcompanyid%2Cphoto&archived=0&idProperty=email` resulted in a `404 Not Found` response\"} {\"correlation_id\":\"8f14fe33-00cc-4910-996e-efa541fc5bb7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [Prospect match] API returned empty result, caching the miss with empty prospect data {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"8f14fe33-00cc-4910-996e-efa541fc5bb7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1364,\"provider\":\"google\",\"responseBody\":{\"error\":\"unauthorized_client\",\"error_description\":\"Unauthorized\"}} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1364,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1370,\"provider\":\"office\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1370,\"provider\":\"office\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1370,\"provider\":\"office\",\"refreshToken\":\"b7ee8035306d0043cea6e00e7c4fe14f745e44074a1194db62a31cdf8b70af3e\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.INFO: [Hubspot] Pagination completed {\"team_id\":2,\"endpoint\":\"https://api.hubapi.com/crm/v3/objects/contact/search\",\"total_requests\":1,\"total_records_fetched\":0,\"total_elapsed_seconds\":0.21,\"average_seconds_per_request\":0.21} {\"correlation_id\":\"8f14fe33-00cc-4910-996e-efa541fc5bb7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"8f14fe33-00cc-4910-996e-efa541fc5bb7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [Prospect match] Cache miss {\"identifier_type\":\"domain\",\"identifier\":\"jiminny.com\",\"crm\":\"hubspot\"} {\"correlation_id\":\"8f14fe33-00cc-4910-996e-efa541fc5bb7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [Prospect match] Cache miss, calling the API {\"identifier_type\":\"domain\",\"identifier\":\"jiminny.com\"} {\"correlation_id\":\"8f14fe33-00cc-4910-996e-efa541fc5bb7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1370,\"provider\":\"office\",\"responseBody\":\"{\\\"error\\\":\\\"invalid_client\\\",\\\"error_description\\\":\\\"AADSTS7000215: Invalid client secret provided. Ensure the secret being sent in the request is the client secret value, not the client secret ID, for a secret added to app 'bbcbb2ef-6200-4fae-82bd-d81f5dd738da'. Trace ID: 329b429c-bad8-4f19-bace-40e48a111900 Correlation ID: 4f12c9af-e118-468c-8441-ebc325071d4b Timestamp: 2026-05-11 10:43:36Z\\\",\\\"error_codes\\\":[7000215],\\\"timestamp\\\":\\\"2026-05-11 10:43:36Z\\\",\\\"trace_id\\\":\\\"329b429c-bad8-4f19-bace-40e48a111900\\\",\\\"correlation_id\\\":\\\"4f12c9af-e118-468c-8441-ebc325071d4b\\\",\\\"error_uri\\\":\\\"https://login.microsoftonline.com/error?code=7000215\\\"}\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1370,\"provider\":\"office\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1202,\"provider\":\"office\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1202,\"provider\":\"office\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1202,\"provider\":\"office\",\"refreshToken\":\"b458799ccc29b21a6e2eb5260fdb63e49ccba21bf942a3973fb63799bd7f0afe\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.INFO: [HubSpot] importAccount {\"crm_provider_id\":\"749766179\",\"config_id\":2} {\"correlation_id\":\"8f14fe33-00cc-4910-996e-efa541fc5bb7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [HubSpot] CRM Search requested {\"request\":{\"filterGroups\":[{\"filters\":[{\"propertyName\":\"associations.company\",\"operator\":\"EQ\",\"value\":\"749766179\"},{\"propertyName\":\"dealstage\",\"operator\":\"NOT_IN\",\"values\":[\"closedwon\",\"4040964\",\"59247967\"]},{\"propertyName\":\"dealstage\",\"operator\":\"NOT_IN\",\"values\":[\"closedlost\",\"4040965\",\"59247968\"]}]}],\"sorts\":[{\"propertyName\":\"modifieddate\",\"direction\":\"DESCENDING\"}],\"properties\":[\"dealname\",\"amount\",\"hubspot_owner_id\",\"pipeline\",\"dealstage\",\"closedate\",\"deal_currency_code\"],\"limit\":200}} {\"correlation_id\":\"8f14fe33-00cc-4910-996e-efa541fc5bb7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:37] local.INFO: [Hubspot] Pagination completed {\"team_id\":2,\"endpoint\":\"https://api.hubapi.com/crm/v3/objects/deals/search\",\"total_requests\":1,\"total_records_fetched\":10,\"total_elapsed_seconds\":0.24,\"average_seconds_per_request\":0.24} {\"correlation_id\":\"8f14fe33-00cc-4910-996e-efa541fc5bb7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:37] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":611455,\"team_id\":2,\"email\":\"aneliya.angelova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"8f14fe33-00cc-4910-996e-efa541fc5bb7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:37] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":611455,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"8f14fe33-00cc-4910-996e-efa541fc5bb7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:37] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":611455} {\"correlation_id\":\"8f14fe33-00cc-4910-996e-efa541fc5bb7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:37] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":611455,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"8f14fe33-00cc-4910-996e-efa541fc5bb7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:37] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":611455,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"8f14fe33-00cc-4910-996e-efa541fc5bb7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:37] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":611451,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"7ea49a4a-ec21-4b93-b990-d1e79531b72a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:37] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":611451} {\"correlation_id\":\"7ea49a4a-ec21-4b93-b990-d1e79531b72a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:37] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":611451,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"7ea49a4a-ec21-4b93-b990-d1e79531b72a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:37] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":611451,\"participants\":[{\"id\":997955,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":997956,\"user_id\":18,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"7ea49a4a-ec21-4b93-b990-d1e79531b72a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:37] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"7ea49a4a-ec21-4b93-b990-d1e79531b72a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:37] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"7ea49a4a-ec21-4b93-b990-d1e79531b72a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:37] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"7ea49a4a-ec21-4b93-b990-d1e79531b72a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:37] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"7ea49a4a-ec21-4b93-b990-d1e79531b72a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:37] local.INFO: [Prospect match] Cache miss, calling the API {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"7ea49a4a-ec21-4b93-b990-d1e79531b72a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:38] local.INFO: [Hubspot] Failed to fetch contact {\"email\":\"support@staging.jiminny.com\",\"reason\":\"[404] Client error: `GET https://api.hubapi.com/crm/v3/objects/contacts/support%40staging.jiminny.com?properties=email%2Cfirstname%2Clastname%2Ccountry%2Cphone%2Cmobilephone%2Cjobtitle%2Chubspot_owner_id%2Cassociatedcompanyid%2Cphoto&archived=0&idProperty=email` resulted in a `404 Not Found` response\"} {\"correlation_id\":\"7ea49a4a-ec21-4b93-b990-d1e79531b72a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:38] local.INFO: [Prospect match] API returned empty result, caching the miss with empty prospect data {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"7ea49a4a-ec21-4b93-b990-d1e79531b72a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:38] local.INFO: [Hubspot] Pagination completed {\"team_id\":2,\"endpoint\":\"https://api.hubapi.com/crm/v3/objects/contact/search\",\"total_requests\":1,\"total_records_fetched\":0,\"total_elapsed_seconds\":0.22,\"average_seconds_per_request\":0.22} {\"correlation_id\":\"7ea49a4a-ec21-4b93-b990-d1e79531b72a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:38] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"7ea49a4a-ec21-4b93-b990-d1e79531b72a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:38] local.INFO: [Prospect match] Cache miss {\"identifier_type\":\"domain\",\"identifier\":\"jiminny.com\",\"crm\":\"hubspot\"} {\"correlation_id\":\"7ea49a4a-ec21-4b93-b990-d1e79531b72a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:38] local.INFO: [Prospect match] Cache miss, calling the API {\"identifier_type\":\"domain\",\"identifier\":\"jiminny.com\"} {\"correlation_id\":\"7ea49a4a-ec21-4b93-b990-d1e79531b72a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:38] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1202,\"provider\":\"office\",\"responseBody\":\"{\\\"error\\\":\\\"invalid_client\\\",\\\"error_description\\\":\\\"AADSTS7000215: Invalid client secret provided. Ensure the secret being sent in the request is the client secret value, not the client secret ID, for a secret added to app 'bbcbb2ef-6200-4fae-82bd-d81f5dd738da'. Trace ID: e8a4c6ea-4ab7-4915-b7a3-93868da51100 Correlation ID: e670afc0-d33c-4205-924c-76bda5c3d01d Timestamp: 2026-05-11 10:43:38Z\\\",\\\"error_codes\\\":[7000215],\\\"timestamp\\\":\\\"2026-05-11 10:43:38Z\\\",\\\"trace_id\\\":\\\"e8a4c6ea-4ab7-4915-b7a3-93868da51100\\\",\\\"correlation_id\\\":\\\"e670afc0-d33c-4205-924c-76bda5c3d01d\\\",\\\"error_uri\\\":\\\"https://login.microsoftonline.com/error?code=7000215\\\"}\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:38] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:38] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1202,\"provider\":\"office\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:38] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1502,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:38] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1502,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:38] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:38] local.INFO: Calendar sync job dispatched {\"calendar_id\":501} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:38] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1300,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:38] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1300,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:38] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:38] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1300,\"provider\":\"google\",\"refreshToken\":\"4b811db0725fd9602a95943519a7da935e2a5065da7d9ebfcb170752e3e1ddb8\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:38] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1300,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Account has been deleted\"}} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:38] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:38] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1300,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:38] local.INFO: [HubSpot] importAccount {\"crm_provider_id\":\"749766179\",\"config_id\":2} {\"correlation_id\":\"7ea49a4a-ec21-4b93-b990-d1e79531b72a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:38] local.INFO: [HubSpot] CRM Search requested {\"request\":{\"filterGroups\":[{\"filters\":[{\"propertyName\":\"associations.company\",\"operator\":\"EQ\",\"value\":\"749766179\"},{\"propertyName\":\"dealstage\",\"operator\":\"NOT_IN\",\"values\":[\"closedwon\",\"4040964\",\"59247967\"]},{\"propertyName\":\"dealstage\",\"operator\":\"NOT_IN\",\"values\":[\"closedlost\",\"4040965\",\"59247968\"]}]}],\"sorts\":[{\"propertyName\":\"modifieddate\",\"direction\":\"DESCENDING\"}],\"properties\":[\"dealname\",\"amount\",\"hubspot_owner_id\",\"pipeline\",\"dealstage\",\"closedate\",\"deal_currency_code\"],\"limit\":200}} {\"correlation_id\":\"7ea49a4a-ec21-4b93-b990-d1e79531b72a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:38] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1409,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:38] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1409,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:38] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:38] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1409,\"provider\":\"google\",\"refreshToken\":\"e2a3f2d06894894eed1ee87d9db1ace77d4d42ee6e1288a8940ad2c10333b0c4\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:38] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1409,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Bad Request\"}} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:38] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:38] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1409,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:39] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1352,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:39] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1352,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:39] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:39] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1352,\"provider\":\"google\",\"refreshToken\":\"dd4b16b00fdc1216da6b717c02338c073636e29162826b2de6db3f064fc029eb\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:39] local.INFO: [Hubspot] Pagination completed {\"team_id\":2,\"endpoint\":\"https://api.hubapi.com/crm/v3/objects/deals/search\",\"total_requests\":1,\"total_records_fetched\":10,\"total_elapsed_seconds\":0.28,\"average_seconds_per_request\":0.28} {\"correlation_id\":\"7ea49a4a-ec21-4b93-b990-d1e79531b72a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1352,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Token has been expired or revoked.\"}} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:39] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:39] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1352,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:39] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1296,\"provider\":\"office\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:39] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1296,\"provider\":\"office\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:39] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:39] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1296,\"provider\":\"office\",\"refreshToken\":\"011ae723c9d800c674e0b4be76f49fc046dac7d501b66c59ef0d9549cfa56ae5\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:39] local.INFO: [ EsUpdateProcessManager ] Finished updating entities in ES {\"worker\":\"\",\"peak_memory\":\"99.73 MB\",\"elapsed_seconds\":0.9,\"update_target\":\"activities\",\"should_iterate_again\":false} {\"correlation_id\":\"4ad05333-9afb-492e-9f0f-b2909ac45b32\",\"trace_id\":\"3d8feb24-b173-4158-b0a4-4cf33af85066\"}\n[2026-05-11 10:43:39] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":611451,\"team_id\":2,\"email\":\"veselin.kulov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"7ea49a4a-ec21-4b93-b990-d1e79531b72a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":611451,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"7ea49a4a-ec21-4b93-b990-d1e79531b72a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":611451} {\"correlation_id\":\"7ea49a4a-ec21-4b93-b990-d1e79531b72a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":611451,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"7ea49a4a-ec21-4b93-b990-d1e79531b72a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":611451,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"7ea49a4a-ec21-4b93-b990-d1e79531b72a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":611087,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"02dbf6fa-a774-4090-af49-cabec99363d4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":611087} {\"correlation_id\":\"02dbf6fa-a774-4090-af49-cabec99363d4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":611087,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"02dbf6fa-a774-4090-af49-cabec99363d4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":611087,\"participants\":[{\"id\":997368,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":997369,\"user_id\":18,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"02dbf6fa-a774-4090-af49-cabec99363d4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"02dbf6fa-a774-4090-af49-cabec99363d4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"02dbf6fa-a774-4090-af49-cabec99363d4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"02dbf6fa-a774-4090-af49-cabec99363d4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"02dbf6fa-a774-4090-af49-cabec99363d4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"02dbf6fa-a774-4090-af49-cabec99363d4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"02dbf6fa-a774-4090-af49-cabec99363d4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"02dbf6fa-a774-4090-af49-cabec99363d4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":611087,\"team_id\":2,\"email\":\"veselin.kulov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"02dbf6fa-a774-4090-af49-cabec99363d4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":611087,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"02dbf6fa-a774-4090-af49-cabec99363d4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":611087} {\"correlation_id\":\"02dbf6fa-a774-4090-af49-cabec99363d4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":611087,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"02dbf6fa-a774-4090-af49-cabec99363d4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":611087,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"02dbf6fa-a774-4090-af49-cabec99363d4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1296,\"provider\":\"office\",\"responseBody\":\"{\\\"error\\\":\\\"invalid_client\\\",\\\"error_description\\\":\\\"AADSTS7000215: Invalid client secret provided. Ensure the secret being sent in the request is the client secret value, not the client secret ID, for a secret added to app 'bbcbb2ef-6200-4fae-82bd-d81f5dd738da'. Trace ID: 4ff43bd2-d427-4049-964c-57fbcad91a00 Correlation ID: 6e92626f-a36d-4c41-ae7c-3b7296df0110 Timestamp: 2026-05-11 10:43:39Z\\\",\\\"error_codes\\\":[7000215],\\\"timestamp\\\":\\\"2026-05-11 10:43:39Z\\\",\\\"trace_id\\\":\\\"4ff43bd2-d427-4049-964c-57fbcad91a00\\\",\\\"correlation_id\\\":\\\"6e92626f-a36d-4c41-ae7c-3b7296df0110\\\",\\\"error_uri\\\":\\\"https://login.microsoftonline.com/error?code=7000215\\\"}\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:39] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:39] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1296,\"provider\":\"office\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:39] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":611076,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"ae1d456f-4b46-4c79-bb7f-bc2e95d48c9f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":611076} {\"correlation_id\":\"ae1d456f-4b46-4c79-bb7f-bc2e95d48c9f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":611076,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"ae1d456f-4b46-4c79-bb7f-bc2e95d48c9f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":611076,\"participants\":[{\"id\":997346,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":997347,\"user_id\":18,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"ae1d456f-4b46-4c79-bb7f-bc2e95d48c9f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":391,\"provider\":\"office\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:39] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":391,\"provider\":\"office\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:39] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:39] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":391,\"provider\":\"office\",\"refreshToken\":\"00045eebae0f39b34887c6d53f92ae78064f7145e1f4b67754aebd03cfb2d881\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:39] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"ae1d456f-4b46-4c79-bb7f-bc2e95d48c9f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"ae1d456f-4b46-4c79-bb7f-bc2e95d48c9f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"ae1d456f-4b46-4c79-bb7f-bc2e95d48c9f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"ae1d456f-4b46-4c79-bb7f-bc2e95d48c9f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"ae1d456f-4b46-4c79-bb7f-bc2e95d48c9f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"ae1d456f-4b46-4c79-bb7f-bc2e95d48c9f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"ae1d456f-4b46-4c79-bb7f-bc2e95d48c9f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":611076,\"team_id\":2,\"email\":\"veselin.kulov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"ae1d456f-4b46-4c79-bb7f-bc2e95d48c9f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":611076,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"ae1d456f-4b46-4c79-bb7f-bc2e95d48c9f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":611076} {\"correlation_id\":\"ae1d456f-4b46-4c79-bb7f-bc2e95d48c9f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":611076,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"ae1d456f-4b46-4c79-bb7f-bc2e95d48c9f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":611076,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"ae1d456f-4b46-4c79-bb7f-bc2e95d48c9f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1502,\"provider\":\"google\"} {\"correlation_id\":\"0fdd7dce-04e2-4696-ba87-8089a739c81c\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1502,\"provider\":\"google\"} {\"correlation_id\":\"0fdd7dce-04e2-4696-ba87-8089a739c81c\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:40] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"0fdd7dce-04e2-4696-ba87-8089a739c81c\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:40] local.INFO: [Calendar] Processing sync {\"calendarId\":\"a33076c1-8d97-431a-99f0-85c9524e118b\",\"from\":null,\"to\":null,\"delta\":\"CIiFh8TP44kDEIiFh8TP44kDGAUgkZvkzgIokZvkzgI=\",\"last_sync\":\"2024-12-09 07:12:53\",\"dateMode\":\"daily\"} {\"correlation_id\":\"0fdd7dce-04e2-4696-ba87-8089a739c81c\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:40] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"integration-app\",\"crm_owner\":1695,\"team_id\":3143} {\"correlation_id\":\"0fdd7dce-04e2-4696-ba87-8089a739c81c\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1502,\"provider\":\"google\"} {\"correlation_id\":\"0fdd7dce-04e2-4696-ba87-8089a739c81c\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1502,\"provider\":\"google\"} {\"correlation_id\":\"0fdd7dce-04e2-4696-ba87-8089a739c81c\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:40] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"0fdd7dce-04e2-4696-ba87-8089a739c81c\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":610935,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"9f5579a5-e89f-4074-9843-d5c1f123878d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610935} {\"correlation_id\":\"9f5579a5-e89f-4074-9843-d5c1f123878d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610935,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"9f5579a5-e89f-4074-9843-d5c1f123878d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":610935,\"participants\":[{\"id\":997141,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":997142,\"user_id\":18,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"9f5579a5-e89f-4074-9843-d5c1f123878d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"9f5579a5-e89f-4074-9843-d5c1f123878d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"9f5579a5-e89f-4074-9843-d5c1f123878d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"9f5579a5-e89f-4074-9843-d5c1f123878d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"9f5579a5-e89f-4074-9843-d5c1f123878d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"9f5579a5-e89f-4074-9843-d5c1f123878d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"9f5579a5-e89f-4074-9843-d5c1f123878d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"9f5579a5-e89f-4074-9843-d5c1f123878d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":610935,\"team_id\":2,\"email\":\"veselin.kulov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"9f5579a5-e89f-4074-9843-d5c1f123878d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":610935,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"9f5579a5-e89f-4074-9843-d5c1f123878d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610935} {\"correlation_id\":\"9f5579a5-e89f-4074-9843-d5c1f123878d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610935,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"9f5579a5-e89f-4074-9843-d5c1f123878d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":610935,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"9f5579a5-e89f-4074-9843-d5c1f123878d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":610915,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"ee1e1cdc-f0b6-4353-b761-ada15282a954\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610915} {\"correlation_id\":\"ee1e1cdc-f0b6-4353-b761-ada15282a954\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610915,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"ee1e1cdc-f0b6-4353-b761-ada15282a954\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":610915,\"participants\":[{\"id\":997104,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":997105,\"user_id\":18,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"ee1e1cdc-f0b6-4353-b761-ada15282a954\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"ee1e1cdc-f0b6-4353-b761-ada15282a954\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"ee1e1cdc-f0b6-4353-b761-ada15282a954\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"ee1e1cdc-f0b6-4353-b761-ada15282a954\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"ee1e1cdc-f0b6-4353-b761-ada15282a954\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"ee1e1cdc-f0b6-4353-b761-ada15282a954\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"ee1e1cdc-f0b6-4353-b761-ada15282a954\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"ee1e1cdc-f0b6-4353-b761-ada15282a954\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":610915,\"team_id\":2,\"email\":\"veselin.kulov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"ee1e1cdc-f0b6-4353-b761-ada15282a954\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":610915,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"ee1e1cdc-f0b6-4353-b761-ada15282a954\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610915} {\"correlation_id\":\"ee1e1cdc-f0b6-4353-b761-ada15282a954\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610915,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"ee1e1cdc-f0b6-4353-b761-ada15282a954\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":610915,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"ee1e1cdc-f0b6-4353-b761-ada15282a954\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":610900,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"46958b32-a07f-4aa9-a052-c6a1e3bf70b2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610900} {\"correlation_id\":\"46958b32-a07f-4aa9-a052-c6a1e3bf70b2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610900,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"46958b32-a07f-4aa9-a052-c6a1e3bf70b2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":610900,\"participants\":[{\"id\":997081,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":997082,\"user_id\":18,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"46958b32-a07f-4aa9-a052-c6a1e3bf70b2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"46958b32-a07f-4aa9-a052-c6a1e3bf70b2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"46958b32-a07f-4aa9-a052-c6a1e3bf70b2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"46958b32-a07f-4aa9-a052-c6a1e3bf70b2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"46958b32-a07f-4aa9-a052-c6a1e3bf70b2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"46958b32-a07f-4aa9-a052-c6a1e3bf70b2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"46958b32-a07f-4aa9-a052-c6a1e3bf70b2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"46958b32-a07f-4aa9-a052-c6a1e3bf70b2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":610900,\"team_id\":2,\"email\":\"veselin.kulov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"46958b32-a07f-4aa9-a052-c6a1e3bf70b2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":610900,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"46958b32-a07f-4aa9-a052-c6a1e3bf70b2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610900} {\"correlation_id\":\"46958b32-a07f-4aa9-a052-c6a1e3bf70b2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610900,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"46958b32-a07f-4aa9-a052-c6a1e3bf70b2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":610900,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"46958b32-a07f-4aa9-a052-c6a1e3bf70b2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":610885,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"b9dc6bd0-a276-4d4c-ad70-8cd444119160\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610885} {\"correlation_id\":\"b9dc6bd0-a276-4d4c-ad70-8cd444119160\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610885,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"b9dc6bd0-a276-4d4c-ad70-8cd444119160\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":610885,\"participants\":[{\"id\":997051,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":997052,\"user_id\":18,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"b9dc6bd0-a276-4d4c-ad70-8cd444119160\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"b9dc6bd0-a276-4d4c-ad70-8cd444119160\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"b9dc6bd0-a276-4d4c-ad70-8cd444119160\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"b9dc6bd0-a276-4d4c-ad70-8cd444119160\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"b9dc6bd0-a276-4d4c-ad70-8cd444119160\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"b9dc6bd0-a276-4d4c-ad70-8cd444119160\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"b9dc6bd0-a276-4d4c-ad70-8cd444119160\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"b9dc6bd0-a276-4d4c-ad70-8cd444119160\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":610885,\"team_id\":2,\"email\":\"veselin.kulov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"b9dc6bd0-a276-4d4c-ad70-8cd444119160\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":610885,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"b9dc6bd0-a276-4d4c-ad70-8cd444119160\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610885} {\"correlation_id\":\"b9dc6bd0-a276-4d4c-ad70-8cd444119160\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610885,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"b9dc6bd0-a276-4d4c-ad70-8cd444119160\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":610885,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"b9dc6bd0-a276-4d4c-ad70-8cd444119160\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":610878,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"117202f1-4432-416d-922c-d7cd0f2ae201\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610878} {\"correlation_id\":\"117202f1-4432-416d-922c-d7cd0f2ae201\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610878,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"117202f1-4432-416d-922c-d7cd0f2ae201\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":610878,\"participants\":[{\"id\":997035,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":997036,\"user_id\":18,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"117202f1-4432-416d-922c-d7cd0f2ae201\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":391,\"provider\":\"office\",\"responseBody\":\"{\\\"error\\\":\\\"invalid_client\\\",\\\"error_description\\\":\\\"AADSTS7000215: Invalid client secret provided. Ensure the secret being sent in the request is the client secret value, not the client secret ID, for a secret added to app 'bbcbb2ef-6200-4fae-82bd-d81f5dd738da'. Trace ID: 72b211a8-6a7c-4954-b720-f1a47e981b00 Correlation ID: 5f9781fe-b28a-44ce-94e3-42c683cdfd60 Timestamp: 2026-05-11 10:43:40Z\\\",\\\"error_codes\\\":[7000215],\\\"timestamp\\\":\\\"2026-05-11 10:43:40Z\\\",\\\"trace_id\\\":\\\"72b211a8-6a7c-4954-b720-f1a47e981b00\\\",\\\"correlation_id\\\":\\\"5f9781fe-b28a-44ce-94e3-42c683cdfd60\\\",\\\"error_uri\\\":\\\"https://login.microsoftonline.com/error?code=7000215\\\"}\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:40] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":391,\"provider\":\"office\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"117202f1-4432-416d-922c-d7cd0f2ae201\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"117202f1-4432-416d-922c-d7cd0f2ae201\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"117202f1-4432-416d-922c-d7cd0f2ae201\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"117202f1-4432-416d-922c-d7cd0f2ae201\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1271,\"provider\":\"office\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1271,\"provider\":\"office\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:40] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"117202f1-4432-416d-922c-d7cd0f2ae201\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1271,\"provider\":\"office\",\"refreshToken\":\"118cde2c06993147b07ccaec4cbcd5026a819dea6c71081166a492933e392afb\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"117202f1-4432-416d-922c-d7cd0f2ae201\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"117202f1-4432-416d-922c-d7cd0f2ae201\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":610878,\"team_id\":2,\"email\":\"veselin.kulov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"117202f1-4432-416d-922c-d7cd0f2ae201\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":610878,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"117202f1-4432-416d-922c-d7cd0f2ae201\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610878} {\"correlation_id\":\"117202f1-4432-416d-922c-d7cd0f2ae201\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610878,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"117202f1-4432-416d-922c-d7cd0f2ae201\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":610878,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"117202f1-4432-416d-922c-d7cd0f2ae201\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Google Calendar] Failed to watch channel for calendar {\"calendarId\":\"a33076c1-8d97-431a-99f0-85c9524e118b\",\"code\":400,\"reason\":\"{\n \\\"error\\\": {\n \\\"errors\\\": [\n {\n \\\"domain\\\": \\\"global\\\",\n \\\"reason\\\": \\\"push.webhookUrlNotHttps\\\",\n \\\"message\\\": \\\"WebHook callback must be HTTPS: /webhook/calendar/google?resourceType=event\\\"\n }\n ],\n \\\"code\\\": 400,\n \\\"message\\\": \\\"WebHook callback must be HTTPS: /webhook/calendar/google?resourceType=event\\\"\n }\n}\"} {\"correlation_id\":\"0fdd7dce-04e2-4696-ba87-8089a739c81c\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:40] local.WARNING: [Calendar] Sync failed {\"calendarId\":\"a33076c1-8d97-431a-99f0-85c9524e118b\",\"code\":400,\"reason\":\"{\n \\\"error\\\": {\n \\\"errors\\\": [\n {\n \\\"domain\\\": \\\"global\\\",\n \\\"reason\\\": \\\"push.webhookUrlNotHttps\\\",\n \\\"message\\\": \\\"WebHook callback must be HTTPS: /webhook/calendar/google?resourceType=event\\\"\n }\n ],\n \\\"code\\\": 400,\n \\\"message\\\": \\\"WebHook callback must be HTTPS: /webhook/calendar/google?resourceType=event\\\"\n }\n}\"} {\"correlation_id\":\"0fdd7dce-04e2-4696-ba87-8089a739c81c\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":610874,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"6264f69a-8461-4d85-9725-ef4c30fb5d35\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610874} {\"correlation_id\":\"6264f69a-8461-4d85-9725-ef4c30fb5d35\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610874,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"6264f69a-8461-4d85-9725-ef4c30fb5d35\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":610874,\"participants\":[{\"id\":997025,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":997026,\"user_id\":18,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"6264f69a-8461-4d85-9725-ef4c30fb5d35\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"6264f69a-8461-4d85-9725-ef4c30fb5d35\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"6264f69a-8461-4d85-9725-ef4c30fb5d35\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"6264f69a-8461-4d85-9725-ef4c30fb5d35\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"6264f69a-8461-4d85-9725-ef4c30fb5d35\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"6264f69a-8461-4d85-9725-ef4c30fb5d35\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"6264f69a-8461-4d85-9725-ef4c30fb5d35\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"6264f69a-8461-4d85-9725-ef4c30fb5d35\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":610874,\"team_id\":2,\"email\":\"veselin.kulov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"6264f69a-8461-4d85-9725-ef4c30fb5d35\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":610874,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"6264f69a-8461-4d85-9725-ef4c30fb5d35\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610874} {\"correlation_id\":\"6264f69a-8461-4d85-9725-ef4c30fb5d35\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610874,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"6264f69a-8461-4d85-9725-ef4c30fb5d35\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":610874,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"6264f69a-8461-4d85-9725-ef4c30fb5d35\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":610867,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"978698d8-e33a-4d4a-b374-04919cae6ac2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610867} {\"correlation_id\":\"978698d8-e33a-4d4a-b374-04919cae6ac2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610867,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"978698d8-e33a-4d4a-b374-04919cae6ac2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":610867,\"participants\":[{\"id\":997011,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":997012,\"user_id\":18,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"978698d8-e33a-4d4a-b374-04919cae6ac2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"978698d8-e33a-4d4a-b374-04919cae6ac2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"978698d8-e33a-4d4a-b374-04919cae6ac2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"978698d8-e33a-4d4a-b374-04919cae6ac2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"978698d8-e33a-4d4a-b374-04919cae6ac2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"978698d8-e33a-4d4a-b374-04919cae6ac2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"978698d8-e33a-4d4a-b374-04919cae6ac2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"978698d8-e33a-4d4a-b374-04919cae6ac2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":610867,\"team_id\":2,\"email\":\"veselin.kulov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"978698d8-e33a-4d4a-b374-04919cae6ac2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":610867,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"978698d8-e33a-4d4a-b374-04919cae6ac2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610867} {\"correlation_id\":\"978698d8-e33a-4d4a-b374-04919cae6ac2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610867,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"978698d8-e33a-4d4a-b374-04919cae6ac2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":610867,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"978698d8-e33a-4d4a-b374-04919cae6ac2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":610764,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"3e294e41-3a19-4afe-87b3-4f7f0336107c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610764} {\"correlation_id\":\"3e294e41-3a19-4afe-87b3-4f7f0336107c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610764,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"3e294e41-3a19-4afe-87b3-4f7f0336107c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":610764,\"participants\":[{\"id\":996951,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":996952,\"user_id\":18,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"3e294e41-3a19-4afe-87b3-4f7f0336107c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"3e294e41-3a19-4afe-87b3-4f7f0336107c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"3e294e41-3a19-4afe-87b3-4f7f0336107c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"3e294e41-3a19-4afe-87b3-4f7f0336107c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"3e294e41-3a19-4afe-87b3-4f7f0336107c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"3e294e41-3a19-4afe-87b3-4f7f0336107c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"3e294e41-3a19-4afe-87b3-4f7f0336107c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"3e294e41-3a19-4afe-87b3-4f7f0336107c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":610764,\"team_id\":2,\"email\":\"veselin.kulov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"3e294e41-3a19-4afe-87b3-4f7f0336107c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":610764,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"3e294e41-3a19-4afe-87b3-4f7f0336107c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610764} {\"correlation_id\":\"3e294e41-3a19-4afe-87b3-4f7f0336107c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610764,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"3e294e41-3a19-4afe-87b3-4f7f0336107c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":610764,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"3e294e41-3a19-4afe-87b3-4f7f0336107c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":610617,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"732b2bbb-ee7f-4bb2-85b1-e6478955544b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610617} {\"correlation_id\":\"732b2bbb-ee7f-4bb2-85b1-e6478955544b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610617,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"732b2bbb-ee7f-4bb2-85b1-e6478955544b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":610617,\"participants\":[{\"id\":996641,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":996642,\"user_id\":18,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"732b2bbb-ee7f-4bb2-85b1-e6478955544b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"732b2bbb-ee7f-4bb2-85b1-e6478955544b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"732b2bbb-ee7f-4bb2-85b1-e6478955544b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"732b2bbb-ee7f-4bb2-85b1-e6478955544b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"732b2bbb-ee7f-4bb2-85b1-e6478955544b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"732b2bbb-ee7f-4bb2-85b1-e6478955544b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"732b2bbb-ee7f-4bb2-85b1-e6478955544b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"732b2bbb-ee7f-4bb2-85b1-e6478955544b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":610617,\"team_id\":2,\"email\":\"veselin.kulov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"732b2bbb-ee7f-4bb2-85b1-e6478955544b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":610617,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"732b2bbb-ee7f-4bb2-85b1-e6478955544b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610617} {\"correlation_id\":\"732b2bbb-ee7f-4bb2-85b1-e6478955544b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610617,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"732b2bbb-ee7f-4bb2-85b1-e6478955544b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":610617,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"732b2bbb-ee7f-4bb2-85b1-e6478955544b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":610539,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"d94f0ac2-c007-4b7d-9fd3-27fc32f9b9a2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610539} {\"correlation_id\":\"d94f0ac2-c007-4b7d-9fd3-27fc32f9b9a2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610539,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"d94f0ac2-c007-4b7d-9fd3-27fc32f9b9a2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":610539,\"participants\":[{\"id\":996485,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":996486,\"user_id\":18,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"d94f0ac2-c007-4b7d-9fd3-27fc32f9b9a2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"d94f0ac2-c007-4b7d-9fd3-27fc32f9b9a2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"d94f0ac2-c007-4b7d-9fd3-27fc32f9b9a2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d94f0ac2-c007-4b7d-9fd3-27fc32f9b9a2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"d94f0ac2-c007-4b7d-9fd3-27fc32f9b9a2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"d94f0ac2-c007-4b7d-9fd3-27fc32f9b9a2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"d94f0ac2-c007-4b7d-9fd3-27fc32f9b9a2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"d94f0ac2-c007-4b7d-9fd3-27fc32f9b9a2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":610539,\"team_id\":2,\"email\":\"veselin.kulov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"d94f0ac2-c007-4b7d-9fd3-27fc32f9b9a2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":610539,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"d94f0ac2-c007-4b7d-9fd3-27fc32f9b9a2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610539} {\"correlation_id\":\"d94f0ac2-c007-4b7d-9fd3-27fc32f9b9a2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610539,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"d94f0ac2-c007-4b7d-9fd3-27fc32f9b9a2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":610539,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"d94f0ac2-c007-4b7d-9fd3-27fc32f9b9a2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":610528,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"c316e103-4e48-4f28-8bc6-206714fc00ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610528} {\"correlation_id\":\"c316e103-4e48-4f28-8bc6-206714fc00ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610528,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"c316e103-4e48-4f28-8bc6-206714fc00ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":610528,\"participants\":[{\"id\":996463,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":996464,\"user_id\":18,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"c316e103-4e48-4f28-8bc6-206714fc00ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"c316e103-4e48-4f28-8bc6-206714fc00ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"c316e103-4e48-4f28-8bc6-206714fc00ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"c316e103-4e48-4f28-8bc6-206714fc00ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"c316e103-4e48-4f28-8bc6-206714fc00ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"c316e103-4e48-4f28-8bc6-206714fc00ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"c316e103-4e48-4f28-8bc6-206714fc00ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"c316e103-4e48-4f28-8bc6-206714fc00ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":610528,\"team_id\":2,\"email\":\"veselin.kulov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"c316e103-4e48-4f28-8bc6-206714fc00ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":610528,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"c316e103-4e48-4f28-8bc6-206714fc00ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610528} {\"correlation_id\":\"c316e103-4e48-4f28-8bc6-206714fc00ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610528,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"c316e103-4e48-4f28-8bc6-206714fc00ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":610528,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"c316e103-4e48-4f28-8bc6-206714fc00ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":610506,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"f83f62c6-544e-4551-8bc2-99f405d022e6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610506} {\"correlation_id\":\"f83f62c6-544e-4551-8bc2-99f405d022e6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610506,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"f83f62c6-544e-4551-8bc2-99f405d022e6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":610506,\"participants\":[{\"id\":996419,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":996420,\"user_id\":18,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"f83f62c6-544e-4551-8bc2-99f405d022e6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"f83f62c6-544e-4551-8bc2-99f405d022e6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"f83f62c6-544e-4551-8bc2-99f405d022e6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"f83f62c6-544e-4551-8bc2-99f405d022e6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"f83f62c6-544e-4551-8bc2-99f405d022e6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"f83f62c6-544e-4551-8bc2-99f405d022e6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"f83f62c6-544e-4551-8bc2-99f405d022e6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"f83f62c6-544e-4551-8bc2-99f405d022e6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1271,\"provider\":\"office\",\"responseBody\":\"{\\\"error\\\":\\\"invalid_client\\\",\\\"error_description\\\":\\\"AADSTS7000215: Invalid client secret provided. Ensure the secret being sent in the request is the client secret value, not the client secret ID, for a secret added to app 'bbcbb2ef-6200-4fae-82bd-d81f5dd738da'. Trace ID: 8addf5ba-9879-4822-b2bb-e5002b0d2200 Correlation ID: feece826-abbe-4c27-85d0-0cddd6b33c3b Timestamp: 2026-05-11 10:43:41Z\\\",\\\"error_codes\\\":[7000215],\\\"timestamp\\\":\\\"2026-05-11 10:43:41Z\\\",\\\"trace_id\\\":\\\"8addf5ba-9879-4822-b2bb-e5002b0d2200\\\",\\\"correlation_id\\\":\\\"feece826-abbe-4c27-85d0-0cddd6b33c3b\\\",\\\"error_uri\\\":\\\"https://login.microsoftonline.com/error?code=7000215\\\"}\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1271,\"provider\":\"office\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":610506,\"team_id\":2,\"email\":\"veselin.kulov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"f83f62c6-544e-4551-8bc2-99f405d022e6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":610506,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"f83f62c6-544e-4551-8bc2-99f405d022e6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610506} {\"correlation_id\":\"f83f62c6-544e-4551-8bc2-99f405d022e6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610506,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"f83f62c6-544e-4551-8bc2-99f405d022e6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1351,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":610506,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"f83f62c6-544e-4551-8bc2-99f405d022e6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1351,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1351,\"provider\":\"google\",\"refreshToken\":\"4271d15b9e60a606439caddc68337f783e472c85b03dacff14d1b6dfded9051c\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":610497,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"854a8b20-3fc0-4ea6-a7c0-e0ddd599dd3b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610497} {\"correlation_id\":\"854a8b20-3fc0-4ea6-a7c0-e0ddd599dd3b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610497,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"854a8b20-3fc0-4ea6-a7c0-e0ddd599dd3b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":610497,\"participants\":[{\"id\":996401,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":996402,\"user_id\":18,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"854a8b20-3fc0-4ea6-a7c0-e0ddd599dd3b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1351,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Bad Request\"}} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1351,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"854a8b20-3fc0-4ea6-a7c0-e0ddd599dd3b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"854a8b20-3fc0-4ea6-a7c0-e0ddd599dd3b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"854a8b20-3fc0-4ea6-a7c0-e0ddd599dd3b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"854a8b20-3fc0-4ea6-a7c0-e0ddd599dd3b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1366,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1366,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"854a8b20-3fc0-4ea6-a7c0-e0ddd599dd3b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"854a8b20-3fc0-4ea6-a7c0-e0ddd599dd3b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1366,\"provider\":\"google\",\"refreshToken\":\"ae21385059b2eebfd43f68aecd56eccd702a1aabb6598f1f7ab594ed8af491b4\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"854a8b20-3fc0-4ea6-a7c0-e0ddd599dd3b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":610497,\"team_id\":2,\"email\":\"veselin.kulov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"854a8b20-3fc0-4ea6-a7c0-e0ddd599dd3b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":610497,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"854a8b20-3fc0-4ea6-a7c0-e0ddd599dd3b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610497} {\"correlation_id\":\"854a8b20-3fc0-4ea6-a7c0-e0ddd599dd3b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610497,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"854a8b20-3fc0-4ea6-a7c0-e0ddd599dd3b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":610497,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"854a8b20-3fc0-4ea6-a7c0-e0ddd599dd3b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":610490,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"bab4020c-8e0d-42eb-afdb-e6bab67d42c7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610490} {\"correlation_id\":\"bab4020c-8e0d-42eb-afdb-e6bab67d42c7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610490,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"bab4020c-8e0d-42eb-afdb-e6bab67d42c7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":610490,\"participants\":[{\"id\":996385,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":996386,\"user_id\":18,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"bab4020c-8e0d-42eb-afdb-e6bab67d42c7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"bab4020c-8e0d-42eb-afdb-e6bab67d42c7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"bab4020c-8e0d-42eb-afdb-e6bab67d42c7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"bab4020c-8e0d-42eb-afdb-e6bab67d42c7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"bab4020c-8e0d-42eb-afdb-e6bab67d42c7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"bab4020c-8e0d-42eb-afdb-e6bab67d42c7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"bab4020c-8e0d-42eb-afdb-e6bab67d42c7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"bab4020c-8e0d-42eb-afdb-e6bab67d42c7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":610490,\"team_id\":2,\"email\":\"veselin.kulov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"bab4020c-8e0d-42eb-afdb-e6bab67d42c7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":610490,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"bab4020c-8e0d-42eb-afdb-e6bab67d42c7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610490} {\"correlation_id\":\"bab4020c-8e0d-42eb-afdb-e6bab67d42c7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610490,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"bab4020c-8e0d-42eb-afdb-e6bab67d42c7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":610490,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"bab4020c-8e0d-42eb-afdb-e6bab67d42c7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1366,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Bad Request\"}} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1366,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":610470,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"55223d65-4d62-42ac-994b-ae4c8f27cbef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1115,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610470} {\"correlation_id\":\"55223d65-4d62-42ac-994b-ae4c8f27cbef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1115,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610470,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"55223d65-4d62-42ac-994b-ae4c8f27cbef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":610470,\"participants\":[{\"id\":996369,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":996370,\"user_id\":18,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"55223d65-4d62-42ac-994b-ae4c8f27cbef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: Calendar sync job dispatched {\"calendar_id\":378} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1421,\"provider\":\"office\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1421,\"provider\":\"office\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"55223d65-4d62-42ac-994b-ae4c8f27cbef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"55223d65-4d62-42ac-994b-ae4c8f27cbef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"55223d65-4d62-42ac-994b-ae4c8f27cbef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: Calendar sync job dispatched {\"calendar_id\":504} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"55223d65-4d62-42ac-994b-ae4c8f27cbef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"55223d65-4d62-42ac-994b-ae4c8f27cbef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"55223d65-4d62-42ac-994b-ae4c8f27cbef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"55223d65-4d62-42ac-994b-ae4c8f27cbef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.NOTICE: Calendar sync end {\"retrieved_calendars\":31,\"processed_calendars\":3} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"calendar:sync\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":610470,\"team_id\":2,\"email\":\"veselin.kulov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"55223d65-4d62-42ac-994b-ae4c8f27cbef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":610470,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"55223d65-4d62-42ac-994b-ae4c8f27cbef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610470} {\"correlation_id\":\"55223d65-4d62-42ac-994b-ae4c8f27cbef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610470,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"55223d65-4d62-42ac-994b-ae4c8f27cbef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":610470,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"55223d65-4d62-42ac-994b-ae4c8f27cbef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":610462,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"0108a9c2-fb98-43fc-ad71-084c53b40936\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610462} {\"correlation_id\":\"0108a9c2-fb98-43fc-ad71-084c53b40936\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610462,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"0108a9c2-fb98-43fc-ad71-084c53b40936\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":610462,\"participants\":[{\"id\":996353,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":996354,\"user_id\":18,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"0108a9c2-fb98-43fc-ad71-084c53b40936\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"0108a9c2-fb98-43fc-ad71-084c53b40936\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"0108a9c2-fb98-43fc-ad71-084c53b40936\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"0108a9c2-fb98-43fc-ad71-084c53b40936\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"0108a9c2-fb98-43fc-ad71-084c53b40936\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"0108a9c2-fb98-43fc-ad71-084c53b40936\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"0108a9c2-fb98-43fc-ad71-084c53b40936\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"0108a9c2-fb98-43fc-ad71-084c53b40936\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":610462,\"team_id\":2,\"email\":\"veselin.kulov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"0108a9c2-fb98-43fc-ad71-084c53b40936\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":610462,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"0108a9c2-fb98-43fc-ad71-084c53b40936\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610462} {\"correlation_id\":\"0108a9c2-fb98-43fc-ad71-084c53b40936\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610462,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"0108a9c2-fb98-43fc-ad71-084c53b40936\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":610462,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"0108a9c2-fb98-43fc-ad71-084c53b40936\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":610451,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"d1fa1d3a-52aa-496e-8905-56f7157299b1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610451} {\"correlation_id\":\"d1fa1d3a-52aa-496e-8905-56f7157299b1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610451,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"d1fa1d3a-52aa-496e-8905-56f7157299b1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":610451,\"participants\":[{\"id\":996340,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":996341,\"user_id\":18,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"d1fa1d3a-52aa-496e-8905-56f7157299b1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"d1fa1d3a-52aa-496e-8905-56f7157299b1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"d1fa1d3a-52aa-496e-8905-56f7157299b1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d1fa1d3a-52aa-496e-8905-56f7157299b1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"d1fa1d3a-52aa-496e-8905-56f7157299b1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"d1fa1d3a-52aa-496e-8905-56f7157299b1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"d1fa1d3a-52aa-496e-8905-56f7157299b1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"d1fa1d3a-52aa-496e-8905-56f7157299b1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":610451,\"team_id\":2,\"email\":\"veselin.kulov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"d1fa1d3a-52aa-496e-8905-56f7157299b1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":610451,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"d1fa1d3a-52aa-496e-8905-56f7157299b1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610451} {\"correlation_id\":\"d1fa1d3a-52aa-496e-8905-56f7157299b1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610451,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"d1fa1d3a-52aa-496e-8905-56f7157299b1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":610451,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"d1fa1d3a-52aa-496e-8905-56f7157299b1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":610438,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"da3f7962-a65d-4bfe-80bf-c33b4ccd36f5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610438} {\"correlation_id\":\"da3f7962-a65d-4bfe-80bf-c33b4ccd36f5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610438,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"da3f7962-a65d-4bfe-80bf-c33b4ccd36f5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":610438,\"participants\":[{\"id\":996320,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":996321,\"user_id\":18,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"da3f7962-a65d-4bfe-80bf-c33b4ccd36f5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"da3f7962-a65d-4bfe-80bf-c33b4ccd36f5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"da3f7962-a65d-4bfe-80bf-c33b4ccd36f5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"da3f7962-a65d-4bfe-80bf-c33b4ccd36f5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"da3f7962-a65d-4bfe-80bf-c33b4ccd36f5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"da3f7962-a65d-4bfe-80bf-c33b4ccd36f5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"da3f7962-a65d-4bfe-80bf-c33b4ccd36f5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"da3f7962-a65d-4bfe-80bf-c33b4ccd36f5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":610438,\"team_id\":2,\"email\":\"veselin.kulov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"da3f7962-a65d-4bfe-80bf-c33b4ccd36f5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":610438,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"da3f7962-a65d-4bfe-80bf-c33b4ccd36f5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610438} {\"correlation_id\":\"da3f7962-a65d-4bfe-80bf-c33b4ccd36f5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610438,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"da3f7962-a65d-4bfe-80bf-c33b4ccd36f5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":610438,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"da3f7962-a65d-4bfe-80bf-c33b4ccd36f5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":610426,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"da7b40c8-e233-4cf9-a133-31f12397ad10\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610426} {\"correlation_id\":\"da7b40c8-e233-4cf9-a133-31f12397ad10\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610426,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"da7b40c8-e233-4cf9-a133-31f12397ad10\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":610426,\"participants\":[{\"id\":996306,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":996307,\"user_id\":18,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"da7b40c8-e233-4cf9-a133-31f12397ad10\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"da7b40c8-e233-4cf9-a133-31f12397ad10\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"da7b40c8-e233-4cf9-a133-31f12397ad10\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"da7b40c8-e233-4cf9-a133-31f12397ad10\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"da7b40c8-e233-4cf9-a133-31f12397ad10\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"da7b40c8-e233-4cf9-a133-31f12397ad10\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"da7b40c8-e233-4cf9-a133-31f12397ad10\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"da7b40c8-e233-4cf9-a133-31f12397ad10\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":610426,\"team_id\":2,\"email\":\"veselin.kulov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"da7b40c8-e233-4cf9-a133-31f12397ad10\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":610426,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"da7b40c8-e233-4cf9-a133-31f12397ad10\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610426} {\"correlation_id\":\"da7b40c8-e233-4cf9-a133-31f12397ad10\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610426,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"da7b40c8-e233-4cf9-a133-31f12397ad10\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":610426,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"da7b40c8-e233-4cf9-a133-31f12397ad10\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":610403,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"4e1aa6ad-3e68-46fe-8129-19feb67cee5f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610403} {\"correlation_id\":\"4e1aa6ad-3e68-46fe-8129-19feb67cee5f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610403,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"4e1aa6ad-3e68-46fe-8129-19feb67cee5f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":610403,\"participants\":[{\"id\":996282,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":996283,\"user_id\":18,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"4e1aa6ad-3e68-46fe-8129-19feb67cee5f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"4e1aa6ad-3e68-46fe-8129-19feb67cee5f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"4e1aa6ad-3e68-46fe-8129-19feb67cee5f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4e1aa6ad-3e68-46fe-8129-19feb67cee5f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"4e1aa6ad-3e68-46fe-8129-19feb67cee5f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"4e1aa6ad-3e68-46fe-8129-19feb67cee5f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"4e1aa6ad-3e68-46fe-8129-19feb67cee5f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"4e1aa6ad-3e68-46fe-8129-19feb67cee5f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":610403,\"team_id\":2,\"email\":\"veselin.kulov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"4e1aa6ad-3e68-46fe-8129-19feb67cee5f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":610403,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"4e1aa6ad-3e68-46fe-8129-19feb67cee5f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610403} {\"correlation_id\":\"4e1aa6ad-3e68-46fe-8129-19feb67cee5f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610403,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"4e1aa6ad-3e68-46fe-8129-19feb67cee5f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":610403,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"4e1aa6ad-3e68-46fe-8129-19feb67cee5f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":610400,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":350,\"stage_id\":34}} {\"correlation_id\":\"c137d466-9e9c-4af1-82c0-9f8639805989\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610400} {\"correlation_id\":\"c137d466-9e9c-4af1-82c0-9f8639805989\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610400,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"c137d466-9e9c-4af1-82c0-9f8639805989\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":610400,\"participants\":[{\"id\":996275,\"user_id\":1460,\"contact_id\":null,\"lead_id\":null},{\"id\":996276,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":996277,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null}]} {\"correlation_id\":\"c137d466-9e9c-4af1-82c0-9f8639805989\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"c137d466-9e9c-4af1-82c0-9f8639805989\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"c137d466-9e9c-4af1-82c0-9f8639805989\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"c137d466-9e9c-4af1-82c0-9f8639805989\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"c137d466-9e9c-4af1-82c0-9f8639805989\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":610400,\"team_id\":2,\"email\":\"aneliya.angelova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"c137d466-9e9c-4af1-82c0-9f8639805989\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":610400,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"c137d466-9e9c-4af1-82c0-9f8639805989\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: ProspectCache - Searching DB for opportunity by owner {\"account_id\":244,\"contact_id\":4487,\"owner_id\":1460} {\"correlation_id\":\"c137d466-9e9c-4af1-82c0-9f8639805989\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: ProspectCache - Fallback DB opportunity search {\"account_id\":244,\"contact_id\":4487} {\"correlation_id\":\"c137d466-9e9c-4af1-82c0-9f8639805989\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: ProspectCache - Opportunity DB search results {\"account_id\":244,\"contact_id\":4487,\"opportunity_id\":350} {\"correlation_id\":\"c137d466-9e9c-4af1-82c0-9f8639805989\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"c137d466-9e9c-4af1-82c0-9f8639805989\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":610400,\"participants_processed\":3,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"c137d466-9e9c-4af1-82c0-9f8639805989\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610400} {\"correlation_id\":\"c137d466-9e9c-4af1-82c0-9f8639805989\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610400,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"c137d466-9e9c-4af1-82c0-9f8639805989\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":610400,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":350,\"stage_id\":34} {\"correlation_id\":\"c137d466-9e9c-4af1-82c0-9f8639805989\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1115,\"provider\":\"google\"} {\"correlation_id\":\"003a1645-a7b4-447f-8217-b1f109d902a1\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:42] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1115,\"provider\":\"google\"} {\"correlation_id\":\"003a1645-a7b4-447f-8217-b1f109d902a1\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:42] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"003a1645-a7b4-447f-8217-b1f109d902a1\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:42] local.INFO: [Calendar] Processing sync {\"calendarId\":\"2676cb6d-f86c-427e-bf78-591e388e3c1e\",\"from\":null,\"to\":null,\"delta\":\"CJ_x49O3jpIDEJ_x49O3jpIDGAUgw67KlwMow67KlwM=\",\"last_sync\":\"2026-01-19 07:48:40\",\"dateMode\":\"daily\"} {\"correlation_id\":\"003a1645-a7b4-447f-8217-b1f109d902a1\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:42] local.WARNING: [Pipedrive] Account not connected for user {\"userId\":\"e6538737-e7b4-455f-a37a-3e79b665a220\",\"account\":{\"Jiminny\\\\Models\\\\SocialAccount\":{\"id\":1116,\"sociable_id\":241,\"provider_user_id\":\"19555731\",\"expires\":1775683749,\"refresh_token_expires\":null,\"provider\":\"pipedrive\",\"state\":\"full-refresh\",\"auth_scope\":\"base,deals:full,activities:full,contacts:full,search:read\",\"retry_after\":null,\"created_at\":\"2023-09-08 09:44:29\",\"updated_at\":\"2026-04-08 22:58:34\"}}} {\"correlation_id\":\"003a1645-a7b4-447f-8217-b1f109d902a1\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:42] local.INFO: [CrmOwnerResolver] Integration owner is not connected, attempting team members {\"crm_provider\":\"pipedrive\",\"crm_owner\":241,\"team_id\":19} {\"correlation_id\":\"003a1645-a7b4-447f-8217-b1f109d902a1\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:42] local.INFO: [CrmOwnerResolver] No team members found with active crm connection {\"crm_provider\":\"pipedrive\",\"team_id\":19} {\"correlation_id\":\"003a1645-a7b4-447f-8217-b1f109d902a1\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:42] local.INFO: [CrmOwnerResolver] No team member found with active crm connection {\"crm_provider\":\"pipedrive\",\"team_id\":19} {\"correlation_id\":\"003a1645-a7b4-447f-8217-b1f109d902a1\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:42] local.WARNING: [Calendar] CRM disconnected for user so events will not be matched {\"provider\":\"pipedrive\",\"user_id\":241,\"message\":\"Your Pipedrive account has become disconnected. Please login to Jiminny to reconnect.\"} {\"correlation_id\":\"003a1645-a7b4-447f-8217-b1f109d902a1\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:42] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1115,\"provider\":\"google\"} {\"correlation_id\":\"003a1645-a7b4-447f-8217-b1f109d902a1\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:42] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1115,\"provider\":\"google\"} {\"correlation_id\":\"003a1645-a7b4-447f-8217-b1f109d902a1\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:42] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"003a1645-a7b4-447f-8217-b1f109d902a1\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:42] local.INFO: [Google Calendar] Failed to watch channel for calendar {\"calendarId\":\"2676cb6d-f86c-427e-bf78-591e388e3c1e\",\"code\":400,\"reason\":\"{\n \\\"error\\\": {\n \\\"errors\\\": [\n {\n \\\"domain\\\": \\\"global\\\",\n \\\"reason\\\": \\\"push.webhookUrlNotHttps\\\",\n \\\"message\\\": \\\"WebHook callback must be HTTPS: /webhook/calendar/google?resourceType=event\\\"\n }\n ],\n \\\"code\\\": 400,\n \\\"message\\\": \\\"WebHook callback must be HTTPS: /webhook/calendar/google?resourceType=event\\\"\n }\n}\"} {\"correlation_id\":\"003a1645-a7b4-447f-8217-b1f109d902a1\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:42] local.WARNING: [Calendar] Sync failed {\"calendarId\":\"2676cb6d-f86c-427e-bf78-591e388e3c1e\",\"code\":400,\"reason\":\"{\n \\\"error\\\": {\n \\\"errors\\\": [\n {\n \\\"domain\\\": \\\"global\\\",\n \\\"reason\\\": \\\"push.webhookUrlNotHttps\\\",\n \\\"message\\\": \\\"WebHook callback must be HTTPS: /webhook/calendar/google?resourceType=event\\\"\n }\n ],\n \\\"code\\\": 400,\n \\\"message\\\": \\\"WebHook callback must be HTTPS: /webhook/calendar/google?resourceType=event\\\"\n }\n}\"} {\"correlation_id\":\"003a1645-a7b4-447f-8217-b1f109d902a1\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:42] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1421,\"provider\":\"office\"} {\"correlation_id\":\"f66a7935-f1a6-4d93-b53d-1464e6e79019\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:42] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1421,\"provider\":\"office\"} {\"correlation_id\":\"f66a7935-f1a6-4d93-b53d-1464e6e79019\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:42] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"f66a7935-f1a6-4d93-b53d-1464e6e79019\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:42] local.INFO: [Calendar] Processing sync {\"calendarId\":\"9e8b1a2c-1a8f-42bd-b161-810fc0baf540\",\"from\":null,\"to\":null,\"delta\":\"R0usmcdvmMuZCBYV0hguCLlvcOB4kXlhlC7KgH1SnZwTrZ3faZv1fXPQqJhxe_L9AxWWlb-wASsjGiiWlhsBUg9MFb3ZdlAYerVV_ZirRPbsKWCxEXhybD90arJmok_M4ecGFUQ9_BIGu-c6RAnJy2TRKZ7gPTsJi_8TGceGAuqimlhm4G4mjDLvYVVwImjjU7M3xJvUzL47dLOGNTJCww.k1TST0VEYCgbFOkwa3ysYMi100FtVfkzfqlXLnV6gPg\",\"last_sync\":\"2026-05-11 06:13:36\",\"dateMode\":\"daily\"} {\"correlation_id\":\"f66a7935-f1a6-4d93-b53d-1464e6e79019\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:42] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"f66a7935-f1a6-4d93-b53d-1464e6e79019\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:42] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"f66a7935-f1a6-4d93-b53d-1464e6e79019\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:42] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"f66a7935-f1a6-4d93-b53d-1464e6e79019\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:42] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":89,\"team_id\":2} {\"correlation_id\":\"f66a7935-f1a6-4d93-b53d-1464e6e79019\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:42] local.INFO: [MS Office Calendar] Skipping delta sync for daily mode {\"calendarId\":\"9e8b1a2c-1a8f-42bd-b161-810fc0baf540\"} {\"correlation_id\":\"f66a7935-f1a6-4d93-b53d-1464e6e79019\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}","depth":4,"on_screen":true,"value":"[2026-05-11 10:41:16] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"ee9b9096-b1f1-4e2b-ac17-af0003eb00c0\",\"trace_id\":\"8904c61b-d5b4-44cc-91bf-68f0f73222c0\"}\n[2026-05-11 10:41:16] local.INFO: [EmailSchedule] STARTING batch process {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"ee9b9096-b1f1-4e2b-ac17-af0003eb00c0\",\"trace_id\":\"8904c61b-d5b4-44cc-91bf-68f0f73222c0\"}\n[2026-05-11 10:41:17] local.INFO: [EmailSchedule] FINISHED batch process {\"host\":\"docker_lamp_1\",\"processed\":0} {\"correlation_id\":\"ee9b9096-b1f1-4e2b-ac17-af0003eb00c0\",\"trace_id\":\"8904c61b-d5b4-44cc-91bf-68f0f73222c0\"}\n[2026-05-11 10:41:17] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":60.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"ee9b9096-b1f1-4e2b-ac17-af0003eb00c0\",\"trace_id\":\"8904c61b-d5b4-44cc-91bf-68f0f73222c0\"}\n[2026-05-11 10:41:21] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"crm:sync-hubspot-objects\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"13c8d2de-a598-41db-b4ca-9799a4bee946\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:21] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"crm:sync-hubspot-objects\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":60.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"13c8d2de-a598-41db-b4ca-9799a4bee946\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:21] local.INFO: [SyncHubspotObjects] Starting sync {\"team\":\"b2b115eb-93ce-4d1b-929c-173757df8fba\",\"usage\":24894088,\"real_usage\":65011712,\"pid\":62058} {\"correlation_id\":\"81433689-3fcd-4128-8b21-068ce5ee75be\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:21] local.WARNING: [HubSpot] Account not connected for user {\"userId\":\"33e34a7a-1c02-4f04-87ac-22c3a385e6e3\",\"account\":{\"Jiminny\\\\Models\\\\SocialAccount\":{\"id\":306,\"sociable_id\":109,\"provider_user_id\":\"11348452\",\"expires\":1701077403,\"refresh_token_expires\":null,\"provider\":\"hubspot\",\"state\":\"full-refresh\",\"auth_scope\":null,\"retry_after\":null,\"created_at\":\"2020-09-01 16:59:04\",\"updated_at\":\"2023-11-27 09:30:03\"}}} {\"correlation_id\":\"81433689-3fcd-4128-8b21-068ce5ee75be\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:21] local.INFO: [CrmOwnerResolver] Integration owner is not connected, attempting team members {\"crm_provider\":\"hubspot\",\"crm_owner\":109,\"team_id\":29} {\"correlation_id\":\"81433689-3fcd-4128-8b21-068ce5ee75be\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:21] local.INFO: [CrmOwnerResolver] No team members found with active crm connection {\"crm_provider\":\"hubspot\",\"team_id\":29} {\"correlation_id\":\"81433689-3fcd-4128-8b21-068ce5ee75be\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:21] local.INFO: [CrmOwnerResolver] No team member found with active crm connection {\"crm_provider\":\"hubspot\",\"team_id\":29} {\"correlation_id\":\"81433689-3fcd-4128-8b21-068ce5ee75be\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:21] local.INFO: [SyncHubspotObjects] Sync finished {\"team\":\"b2b115eb-93ce-4d1b-929c-173757df8fba\",\"provider\":\"hubspot\",\"status\":\"disconnected\",\"duration_ms\":35.85,\"usage\":24956128,\"real_usage\":65011712,\"pid\":62058,\"reason\":\"Your HubSpot account has become disconnected. Please login to Jiminny to reconnect.\"} {\"correlation_id\":\"81433689-3fcd-4128-8b21-068ce5ee75be\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:21] local.INFO: [SyncHubspotObjects] Starting sync {\"team\":\"abae74b8-bfa8-4383-9a7f-89f4bf2bdbb4\",\"usage\":24914344,\"real_usage\":65011712,\"pid\":62058} {\"correlation_id\":\"d2cb4aff-14c0-474f-89f2-f5c69dafe934\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:21] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"d2cb4aff-14c0-474f-89f2-f5c69dafe934\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:21] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"d2cb4aff-14c0-474f-89f2-f5c69dafe934\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:21] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d2cb4aff-14c0-474f-89f2-f5c69dafe934\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:21] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"d2cb4aff-14c0-474f-89f2-f5c69dafe934\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:21] local.INFO: [HubSpot] Syncing opportunities using strategy: lastModified {\"team\":2} {\"correlation_id\":\"d2cb4aff-14c0-474f-89f2-f5c69dafe934\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:21] local.INFO: [Hubspot] Pagination completed {\"team_id\":2,\"endpoint\":\"https://api.hubapi.com/crm/v3/objects/deals/search\",\"total_requests\":1,\"total_records_fetched\":0,\"total_elapsed_seconds\":0.26,\"average_seconds_per_request\":0.26} {\"correlation_id\":\"d2cb4aff-14c0-474f-89f2-f5c69dafe934\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:21] local.INFO: [HubSpot] Synced opportunities {\"team\":2,\"strategies\":\"lastModified\",\"sync_count\":0,\"total\":0,\"last_synced_id\":null,\"duration_ms\":288.63} {\"correlation_id\":\"d2cb4aff-14c0-474f-89f2-f5c69dafe934\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:21] local.INFO: [SyncHubspotObjects] Sync finished {\"team\":\"abae74b8-bfa8-4383-9a7f-89f4bf2bdbb4\",\"provider\":\"hubspot\",\"status\":\"completed\",\"duration_ms\":324.99,\"usage\":24978696,\"real_usage\":65011712,\"pid\":62058} {\"correlation_id\":\"d2cb4aff-14c0-474f-89f2-f5c69dafe934\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:22] local.INFO: [SyncHubspotObjects] Starting sync {\"team\":\"c6b9d6b0-b48d-4832-a68c-a57d60651888\",\"usage\":24956624,\"real_usage\":65011712,\"pid\":62058} {\"correlation_id\":\"955d9400-2f7f-4170-be31-43bafa1ef3bd\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:22] local.WARNING: [HubSpot] Account not connected for user {\"userId\":\"71e3aac5-fb66-47c5-a236-2d051ae3e319\",\"account\":null} {\"correlation_id\":\"955d9400-2f7f-4170-be31-43bafa1ef3bd\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:22] local.INFO: [CrmOwnerResolver] Integration owner is not connected, attempting team members {\"crm_provider\":\"hubspot\",\"crm_owner\":256,\"team_id\":49} {\"correlation_id\":\"955d9400-2f7f-4170-be31-43bafa1ef3bd\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:22] local.INFO: [CrmOwnerResolver] No team members found with active crm connection {\"crm_provider\":\"hubspot\",\"team_id\":49} {\"correlation_id\":\"955d9400-2f7f-4170-be31-43bafa1ef3bd\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:22] local.INFO: [CrmOwnerResolver] No team member found with active crm connection {\"crm_provider\":\"hubspot\",\"team_id\":49} {\"correlation_id\":\"955d9400-2f7f-4170-be31-43bafa1ef3bd\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:22] local.INFO: [SyncHubspotObjects] Sync finished {\"team\":\"c6b9d6b0-b48d-4832-a68c-a57d60651888\",\"provider\":\"hubspot\",\"status\":\"disconnected\",\"duration_ms\":17.51,\"usage\":24941048,\"real_usage\":65011712,\"pid\":62058,\"reason\":\"Social account for HubSpot cannot be found. Please login to Jiminny to connect.\"} {\"correlation_id\":\"955d9400-2f7f-4170-be31-43bafa1ef3bd\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:22] local.INFO: [SyncHubspotObjects] Starting sync {\"team\":\"b2d49a54-b645-4637-a7ae-a86cfce6e8e4\",\"usage\":24901688,\"real_usage\":65011712,\"pid\":62058} {\"correlation_id\":\"6d0b5717-9160-4c2f-ad12-56e42a2c544a\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:22] local.WARNING: [HubSpot] Account not connected for user {\"userId\":\"2ac0447f-3c8c-4ce0-baeb-b63ddb76fa9b\",\"account\":null} {\"correlation_id\":\"6d0b5717-9160-4c2f-ad12-56e42a2c544a\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:22] local.INFO: [CrmOwnerResolver] Integration owner is not connected, attempting team members {\"crm_provider\":\"hubspot\",\"crm_owner\":130,\"team_id\":42} {\"correlation_id\":\"6d0b5717-9160-4c2f-ad12-56e42a2c544a\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:22] local.INFO: [CrmOwnerResolver] No team members found with active crm connection {\"crm_provider\":\"hubspot\",\"team_id\":42} {\"correlation_id\":\"6d0b5717-9160-4c2f-ad12-56e42a2c544a\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:22] local.INFO: [CrmOwnerResolver] No team member found with active crm connection {\"crm_provider\":\"hubspot\",\"team_id\":42} {\"correlation_id\":\"6d0b5717-9160-4c2f-ad12-56e42a2c544a\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:22] local.INFO: [SyncHubspotObjects] Sync finished {\"team\":\"b2d49a54-b645-4637-a7ae-a86cfce6e8e4\",\"provider\":\"hubspot\",\"status\":\"disconnected\",\"duration_ms\":10.99,\"usage\":24944632,\"real_usage\":65011712,\"pid\":62058,\"reason\":\"Social account for HubSpot cannot be found. Please login to Jiminny to connect.\"} {\"correlation_id\":\"6d0b5717-9160-4c2f-ad12-56e42a2c544a\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:39] local.INFO: [HubSpot Journal Polling] No data {\"correlation_id\":\"1f8f11e6-0cf5-49dd-b8d7-56f51ec27f8e\",\"trace_id\":\"0f08076e-448d-487e-99a0-e73dae5c0ad4\"}\n[2026-05-11 10:41:39] local.WARNING: [HubSpot Journal Polling] Maximum empty results reached, stopping {\"empty_results\":5,\"max_empty_results\":5} {\"correlation_id\":\"1f8f11e6-0cf5-49dd-b8d7-56f51ec27f8e\",\"trace_id\":\"0f08076e-448d-487e-99a0-e73dae5c0ad4\"}\n[2026-05-11 10:41:39] local.WARNING: [HubSpot Journal Polling] Maximum empty results reached, stopping {\"empty_results\":5,\"max_empty_results\":5} {\"correlation_id\":\"1f8f11e6-0cf5-49dd-b8d7-56f51ec27f8e\",\"trace_id\":\"0f08076e-448d-487e-99a0-e73dae5c0ad4\"}\n[2026-05-11 10:41:39] local.INFO: [HubSpot Journal Polling] Service ending {\"runtime_seconds\":57,\"total_cycles\":5,\"files_downloaded\":0,\"empty_files\":0,\"other_portal_skipped\":0,\"total_events\":0,\"events_per_file\":0,\"avg_api_ms\":296.8,\"avg_download_ms\":0.0,\"avg_transform_ms\":0.0,\"avg_process_ms\":0.0,\"peak_memory_mb\":99.73} {\"correlation_id\":\"1f8f11e6-0cf5-49dd-b8d7-56f51ec27f8e\",\"trace_id\":\"0f08076e-448d-487e-99a0-e73dae5c0ad4\"}\n[2026-05-11 10:41:39] local.INFO: [HubSpot Journal Polling] Saved offset to database on cleanup {\"offset\":\"019e15a9-9ea0-7da7-87bc-82592e3ccf0d\"} {\"correlation_id\":\"1f8f11e6-0cf5-49dd-b8d7-56f51ec27f8e\",\"trace_id\":\"0f08076e-448d-487e-99a0-e73dae5c0ad4\"}\n[2026-05-11 10:41:39] local.INFO: [HubSpot Journal Polling] Released polling lock {\"correlation_id\":\"1f8f11e6-0cf5-49dd-b8d7-56f51ec27f8e\",\"trace_id\":\"0f08076e-448d-487e-99a0-e73dae5c0ad4\"}\n[2026-05-11 10:42:07] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"1aa66921-0a71-4d61-8cd7-0663242dca44\",\"trace_id\":\"f6b1d739-39b6-4043-a81e-a1af2c76d401\"}\n[2026-05-11 10:42:07] local.INFO: [ScheduleBotCommand] Number of activities to be captured: 0 {\"correlation_id\":\"1aa66921-0a71-4d61-8cd7-0663242dca44\",\"trace_id\":\"f6b1d739-39b6-4043-a81e-a1af2c76d401\"}\n[2026-05-11 10:42:07] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":60.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"1aa66921-0a71-4d61-8cd7-0663242dca44\",\"trace_id\":\"f6b1d739-39b6-4043-a81e-a1af2c76d401\"}\n[2026-05-11 10:42:10] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"cc420ba5-39bf-4183-95ae-48962a9c124b\",\"trace_id\":\"8daf6081-5c8a-4fae-98d8-e317d8580bc3\"}\n[2026-05-11 10:42:10] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":60.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"cc420ba5-39bf-4183-95ae-48962a9c124b\",\"trace_id\":\"8daf6081-5c8a-4fae-98d8-e317d8580bc3\"}\n[2026-05-11 10:42:13] local.NOTICE: Monitoring start {\"correlation_id\":\"50696062-587c-4c43-8075-7229916ea46f\",\"trace_id\":\"b58f754c-906f-413b-80e6-f89189dbe078\"}\n[2026-05-11 10:42:13] local.NOTICE: Monitoring end {\"correlation_id\":\"50696062-587c-4c43-8075-7229916ea46f\",\"trace_id\":\"b58f754c-906f-413b-80e6-f89189dbe078\"}\n[2026-05-11 10:42:17] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"f70656cd-6de3-4f8b-bb9d-74a941c552c4\",\"trace_id\":\"c780d8e3-2abd-41a3-a9dc-7c2be6aa22d4\"}\n[2026-05-11 10:42:17] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":60.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"f70656cd-6de3-4f8b-bb9d-74a941c552c4\",\"trace_id\":\"c780d8e3-2abd-41a3-a9dc-7c2be6aa22d4\"}\n[2026-05-11 10:42:20] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"b2e73601-1777-4ddb-a152-fe85f4c89afc\",\"trace_id\":\"8bac5054-216a-4605-b13b-9efab8ab5341\"}\n[2026-05-11 10:42:20] local.INFO: [EmailSchedule] STARTING batch process {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"b2e73601-1777-4ddb-a152-fe85f4c89afc\",\"trace_id\":\"8bac5054-216a-4605-b13b-9efab8ab5341\"}\n[2026-05-11 10:42:20] local.INFO: [EmailSchedule] FINISHED batch process {\"host\":\"docker_lamp_1\",\"processed\":0} {\"correlation_id\":\"b2e73601-1777-4ddb-a152-fe85f4c89afc\",\"trace_id\":\"8bac5054-216a-4605-b13b-9efab8ab5341\"}\n[2026-05-11 10:42:20] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":60.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"b2e73601-1777-4ddb-a152-fe85f4c89afc\",\"trace_id\":\"8bac5054-216a-4605-b13b-9efab8ab5341\"}\n[2026-05-11 10:42:31] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"conference:monitor:count\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"175d1973-e4ab-47f7-bb25-14bc3f0a1cce\",\"trace_id\":\"723f475a-f2fc-4235-8163-c2a8ed5777e6\"}\n[2026-05-11 10:42:31] local.INFO: Running conference:monitor:count command for activities in (2026-05-11 10:40:00, 2026-05-11 10:42:00] {\"correlation_id\":\"175d1973-e4ab-47f7-bb25-14bc3f0a1cce\",\"trace_id\":\"723f475a-f2fc-4235-8163-c2a8ed5777e6\"}\n[2026-05-11 10:42:31] local.INFO: [conference:monitor:count] No activities found in (2026-05-11 10:40:00, 2026-05-11 10:42:00] {\"correlation_id\":\"175d1973-e4ab-47f7-bb25-14bc3f0a1cce\",\"trace_id\":\"723f475a-f2fc-4235-8163-c2a8ed5777e6\"}\n[2026-05-11 10:42:31] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"conference:monitor:count\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":60.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"175d1973-e4ab-47f7-bb25-14bc3f0a1cce\",\"trace_id\":\"723f475a-f2fc-4235-8163-c2a8ed5777e6\"}\n[2026-05-11 10:42:36] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:create\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"4243f43b-9d70-4dd0-9c98-3122eb9445ed\",\"trace_id\":\"0c811e1d-ed7e-4aa7-ba81-a14a0cf4b306\"}\n[2026-05-11 10:42:36] local.INFO: [EmailSchedule] STARTING batch create {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"4243f43b-9d70-4dd0-9c98-3122eb9445ed\",\"trace_id\":\"0c811e1d-ed7e-4aa7-ba81-a14a0cf4b306\"}\n[2026-05-11 10:42:36] local.INFO: [EmailSchedule] FINISHED batch create {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"4243f43b-9d70-4dd0-9c98-3122eb9445ed\",\"trace_id\":\"0c811e1d-ed7e-4aa7-ba81-a14a0cf4b306\"}\n[2026-05-11 10:42:36] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:create\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":60.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"4243f43b-9d70-4dd0-9c98-3122eb9445ed\",\"trace_id\":\"0c811e1d-ed7e-4aa7-ba81-a14a0cf4b306\"}\n[2026-05-11 10:42:39] local.INFO: [Jiminny\\Jobs\\Mailbox\\CreateBatches] processed 2 inboxes and created 0 batches {\"userId\":null,\"batchSize\":30,\"maxBatches\":1000} {\"correlation_id\":\"2328b74c-d9ea-43ef-95db-20ea7c6ec9f5\",\"trace_id\":\"0c811e1d-ed7e-4aa7-ba81-a14a0cf4b306\"}\n[2026-05-11 10:42:54] local.INFO: [HubSpot Webhook] Signature validation started {\"method\":\"POST\",\"uri\":\"/webhook/conference/hubspot/events\",\"has_v3_signature\":true,\"has_v1_signature\":true} {\"correlation_id\":\"35391537-e747-4658-94f7-9d3d8c65eab6\",\"trace_id\":\"163e44b3-9f8e-4957-b04d-4937c33d30e6\"}\n[2026-05-11 10:42:54] local.INFO: [HubSpot Webhook] Event received {\"payload\":[{\"eventId\":1531490889,\"subscriptionId\":5683273,\"portalId\":4392066,\"appId\":38482,\"occurredAt\":1778478181931,\"subscriptionType\":\"company.associationChange\",\"attemptNumber\":8,\"changeSource\":\"USER\",\"associationType\":\"COMPANY_TO_DEAL\",\"fromObjectId\":13266161465,\"toObjectId\":60142058500,\"associationRemoved\":false,\"isPrimaryAssociation\":false}],\"event_count\":1} {\"correlation_id\":\"35391537-e747-4658-94f7-9d3d8c65eab6\",\"trace_id\":\"163e44b3-9f8e-4957-b04d-4937c33d30e6\"}\n[2026-05-11 10:42:54] local.INFO: [HubSpot Webhook Job] Processing webhook events {\"event_count\":1,\"local_count\":1,\"forward_count\":1} {\"correlation_id\":\"bb5ee15c-0826-40ef-a4c4-83761c307c7b\",\"trace_id\":\"163e44b3-9f8e-4957-b04d-4937c33d30e6\"}\n[2026-05-11 10:42:55] local.INFO: [Webhook Forwarder] Forwarding to instance {\"url\":\"https://uranus.staging.jiminny.com/internal/webhook-receiver/hubspot\",\"route\":\"internal.webhook-receiver.hubspot\",\"event_count\":1} {\"correlation_id\":\"bb5ee15c-0826-40ef-a4c4-83761c307c7b\",\"trace_id\":\"163e44b3-9f8e-4957-b04d-4937c33d30e6\"}\n[2026-05-11 10:42:55] local.ERROR: [Webhook Forwarder] Failed to forward to instance {\"url\":\"https://uranus.staging.jiminny.com\",\"route\":\"internal.webhook-receiver.hubspot\",\"error\":\"Server error: `POST https://uranus.staging.jiminny.com/internal/webhook-receiver/hubspot` resulted in a `503 Service Temporarily Unavailable` response:\n<html>\n\n<head><title>503 Service Temporarily Unavailable</title></head>\n\n<body>\n\n<center><h1>503 Service Temporarily Una (truncated...)\n\",\"code\":503} {\"correlation_id\":\"bb5ee15c-0826-40ef-a4c4-83761c307c7b\",\"trace_id\":\"163e44b3-9f8e-4957-b04d-4937c33d30e6\"}\n[2026-05-11 10:42:55] local.INFO: [Webhook Forwarder] Forwarding to instance {\"url\":\"https://app.qai.jiminny.com/internal/webhook-receiver/hubspot\",\"route\":\"internal.webhook-receiver.hubspot\",\"event_count\":1} {\"correlation_id\":\"bb5ee15c-0826-40ef-a4c4-83761c307c7b\",\"trace_id\":\"163e44b3-9f8e-4957-b04d-4937c33d30e6\"}\n[2026-05-11 10:42:56] local.INFO: [Webhook Forwarder] Successfully forwarded to instance {\"url\":\"https://app.qai.jiminny.com/internal/webhook-receiver/hubspot\",\"status_code\":202,\"response\":{\"status\":\"accepted\",\"message\":\"Events queued for processing\",\"event_count\":1}} {\"correlation_id\":\"bb5ee15c-0826-40ef-a4c4-83761c307c7b\",\"trace_id\":\"163e44b3-9f8e-4957-b04d-4937c33d30e6\"}\n[2026-05-11 10:42:56] local.INFO: [BatchSyncCollector] Stored webhook in Redis {\"object_type\":\"company\",\"crm_provider_id\":\"13266161465\",\"event_type\":\"association_change\",\"configuration_id\":2,\"batch_key\":\"batch_sync_company:ids:2:all\",\"was_new\":true,\"current_size\":1} {\"correlation_id\":\"bb5ee15c-0826-40ef-a4c4-83761c307c7b\",\"trace_id\":\"163e44b3-9f8e-4957-b04d-4937c33d30e6\"}\n[2026-05-11 10:43:14] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"a2a4326d-0416-4ea7-8eac-370e0fee93fb\",\"trace_id\":\"49cf2405-4c7b-48ce-9aac-074f08037e94\"}\n[2026-05-11 10:43:14] local.INFO: [ScheduleBotCommand] Number of activities to be captured: 0 {\"correlation_id\":\"a2a4326d-0416-4ea7-8eac-370e0fee93fb\",\"trace_id\":\"49cf2405-4c7b-48ce-9aac-074f08037e94\"}\n[2026-05-11 10:43:14] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":60.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"a2a4326d-0416-4ea7-8eac-370e0fee93fb\",\"trace_id\":\"49cf2405-4c7b-48ce-9aac-074f08037e94\"}\n[2026-05-11 10:43:18] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":615092,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"e7be542b-ee51-44b8-a563-a2eac45b3413\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:18] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":615092} {\"correlation_id\":\"e7be542b-ee51-44b8-a563-a2eac45b3413\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:18] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":615092,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"e7be542b-ee51-44b8-a563-a2eac45b3413\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:18] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":615092,\"participants\":[{\"id\":1004102,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":1004103,\"user_id\":89,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"e7be542b-ee51-44b8-a563-a2eac45b3413\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:19] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"e7be542b-ee51-44b8-a563-a2eac45b3413\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:19] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"e7be542b-ee51-44b8-a563-a2eac45b3413\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:19] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"e7be542b-ee51-44b8-a563-a2eac45b3413\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:19] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"e7be542b-ee51-44b8-a563-a2eac45b3413\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:20] local.INFO: [Prospect match] Cache miss, calling the API {\"identifier_type\":\"email\",\"identifier\":\"nikolay.nikolov@jiminny.com\"} {\"correlation_id\":\"e7be542b-ee51-44b8-a563-a2eac45b3413\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:20] local.INFO: [Hubspot] Failed to fetch contact {\"email\":\"nikolay.nikolov@jiminny.com\",\"reason\":\"[404] Client error: `GET https://api.hubapi.com/crm/v3/objects/contacts/nikolay.nikolov%40jiminny.com?properties=email%2Cfirstname%2Clastname%2Ccountry%2Cphone%2Cmobilephone%2Cjobtitle%2Chubspot_owner_id%2Cassociatedcompanyid%2Cphoto&archived=0&idProperty=email` resulted in a `404 Not Found` response\"} {\"correlation_id\":\"e7be542b-ee51-44b8-a563-a2eac45b3413\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:20] local.INFO: [Prospect match] API returned empty result, caching the miss with empty prospect data {\"identifier_type\":\"email\",\"identifier\":\"nikolay.nikolov@jiminny.com\"} {\"correlation_id\":\"e7be542b-ee51-44b8-a563-a2eac45b3413\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:21] local.INFO: [Hubspot] Pagination completed {\"team_id\":2,\"endpoint\":\"https://api.hubapi.com/crm/v3/objects/contact/search\",\"total_requests\":1,\"total_records_fetched\":0,\"total_elapsed_seconds\":0.38,\"average_seconds_per_request\":0.38} {\"correlation_id\":\"e7be542b-ee51-44b8-a563-a2eac45b3413\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:22] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"ab7124d8-7931-41ef-8341-93a3305f65eb\",\"trace_id\":\"4564d24d-bf9e-486b-ac8e-0f69c9b286d8\"}\n[2026-05-11 10:43:22] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":60.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"ab7124d8-7931-41ef-8341-93a3305f65eb\",\"trace_id\":\"4564d24d-bf9e-486b-ac8e-0f69c9b286d8\"}\n[2026-05-11 10:43:22] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"nikolay.nikolov@jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"e7be542b-ee51-44b8-a563-a2eac45b3413\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:22] local.INFO: [Prospect match] Cache miss {\"identifier_type\":\"domain\",\"identifier\":\"jiminny.com\",\"crm\":\"hubspot\"} {\"correlation_id\":\"e7be542b-ee51-44b8-a563-a2eac45b3413\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:22] local.INFO: [Prospect match] Cache miss, calling the API {\"identifier_type\":\"domain\",\"identifier\":\"jiminny.com\"} {\"correlation_id\":\"e7be542b-ee51-44b8-a563-a2eac45b3413\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:23] local.INFO: [HubSpot] importAccount {\"crm_provider_id\":\"749766179\",\"config_id\":2} {\"correlation_id\":\"e7be542b-ee51-44b8-a563-a2eac45b3413\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:23] local.INFO: [HubSpot] CRM Search requested {\"request\":{\"filterGroups\":[{\"filters\":[{\"propertyName\":\"associations.company\",\"operator\":\"EQ\",\"value\":\"749766179\"},{\"propertyName\":\"dealstage\",\"operator\":\"NOT_IN\",\"values\":[\"closedwon\",\"4040964\",\"59247967\"]},{\"propertyName\":\"dealstage\",\"operator\":\"NOT_IN\",\"values\":[\"closedlost\",\"4040965\",\"59247968\"]}]}],\"sorts\":[{\"propertyName\":\"modifieddate\",\"direction\":\"DESCENDING\"}],\"properties\":[\"dealname\",\"amount\",\"hubspot_owner_id\",\"pipeline\",\"dealstage\",\"closedate\",\"deal_currency_code\"],\"limit\":200}} {\"correlation_id\":\"e7be542b-ee51-44b8-a563-a2eac45b3413\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:23] local.INFO: [Hubspot] Pagination completed {\"team_id\":2,\"endpoint\":\"https://api.hubapi.com/crm/v3/objects/deals/search\",\"total_requests\":1,\"total_records_fetched\":10,\"total_elapsed_seconds\":0.3,\"average_seconds_per_request\":0.3} {\"correlation_id\":\"e7be542b-ee51-44b8-a563-a2eac45b3413\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:24] local.INFO: [ EsUpdateProcessManager ] Finished updating entities in ES {\"worker\":\"\",\"peak_memory\":\"99.73 MB\",\"elapsed_seconds\":3.22,\"update_target\":\"activities\",\"should_iterate_again\":false} {\"correlation_id\":\"4ad05333-9afb-492e-9f0f-b2909ac45b32\",\"trace_id\":\"3d8feb24-b173-4158-b0a4-4cf33af85066\"}\n[2026-05-11 10:43:24] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":615092,\"team_id\":2,\"email\":\"nikolay.nikolov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"e7be542b-ee51-44b8-a563-a2eac45b3413\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:24] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":615092,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"e7be542b-ee51-44b8-a563-a2eac45b3413\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:24] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":615092} {\"correlation_id\":\"e7be542b-ee51-44b8-a563-a2eac45b3413\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:24] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":615092,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"e7be542b-ee51-44b8-a563-a2eac45b3413\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:24] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":615092,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"e7be542b-ee51-44b8-a563-a2eac45b3413\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:24] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":614436,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"a34067d1-038f-48bf-af25-c9e09374ce62\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:24] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":614436} {\"correlation_id\":\"a34067d1-038f-48bf-af25-c9e09374ce62\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:24] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":614436,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"a34067d1-038f-48bf-af25-c9e09374ce62\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:24] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":614436,\"participants\":[{\"id\":1002751,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":1002752,\"user_id\":89,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"a34067d1-038f-48bf-af25-c9e09374ce62\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:24] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"a34067d1-038f-48bf-af25-c9e09374ce62\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:24] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"a34067d1-038f-48bf-af25-c9e09374ce62\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:24] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"a34067d1-038f-48bf-af25-c9e09374ce62\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:24] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"a34067d1-038f-48bf-af25-c9e09374ce62\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:24] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"nikolay.nikolov@jiminny.com\"} {\"correlation_id\":\"a34067d1-038f-48bf-af25-c9e09374ce62\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:24] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"nikolay.nikolov@jiminny.com\"} {\"correlation_id\":\"a34067d1-038f-48bf-af25-c9e09374ce62\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:24] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"nikolay.nikolov@jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"a34067d1-038f-48bf-af25-c9e09374ce62\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:24] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":614436,\"team_id\":2,\"email\":\"nikolay.nikolov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"a34067d1-038f-48bf-af25-c9e09374ce62\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:24] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":614436,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"a34067d1-038f-48bf-af25-c9e09374ce62\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:24] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":614436} {\"correlation_id\":\"a34067d1-038f-48bf-af25-c9e09374ce62\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:24] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":614436,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"a34067d1-038f-48bf-af25-c9e09374ce62\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:24] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":614436,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"a34067d1-038f-48bf-af25-c9e09374ce62\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":614382,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"ee88af0b-20d3-4911-a378-bea7fc6dc3f6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":614382} {\"correlation_id\":\"ee88af0b-20d3-4911-a378-bea7fc6dc3f6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":614382,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"ee88af0b-20d3-4911-a378-bea7fc6dc3f6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":614382,\"participants\":[{\"id\":1002632,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":1002633,\"user_id\":89,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"ee88af0b-20d3-4911-a378-bea7fc6dc3f6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"ee88af0b-20d3-4911-a378-bea7fc6dc3f6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"ee88af0b-20d3-4911-a378-bea7fc6dc3f6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"ee88af0b-20d3-4911-a378-bea7fc6dc3f6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"ee88af0b-20d3-4911-a378-bea7fc6dc3f6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"nikolay.nikolov@jiminny.com\"} {\"correlation_id\":\"ee88af0b-20d3-4911-a378-bea7fc6dc3f6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"nikolay.nikolov@jiminny.com\"} {\"correlation_id\":\"ee88af0b-20d3-4911-a378-bea7fc6dc3f6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"nikolay.nikolov@jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"ee88af0b-20d3-4911-a378-bea7fc6dc3f6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":614382,\"team_id\":2,\"email\":\"nikolay.nikolov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"ee88af0b-20d3-4911-a378-bea7fc6dc3f6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":614382,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"ee88af0b-20d3-4911-a378-bea7fc6dc3f6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":614382} {\"correlation_id\":\"ee88af0b-20d3-4911-a378-bea7fc6dc3f6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":614382,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"ee88af0b-20d3-4911-a378-bea7fc6dc3f6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":614382,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"ee88af0b-20d3-4911-a378-bea7fc6dc3f6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":614381,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"535b99f0-d38e-4b9c-bf19-3751ce4e6a1d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":614381} {\"correlation_id\":\"535b99f0-d38e-4b9c-bf19-3751ce4e6a1d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":614381,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"535b99f0-d38e-4b9c-bf19-3751ce4e6a1d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":614381,\"participants\":[{\"id\":1002630,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":1002631,\"user_id\":89,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"535b99f0-d38e-4b9c-bf19-3751ce4e6a1d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"535b99f0-d38e-4b9c-bf19-3751ce4e6a1d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"535b99f0-d38e-4b9c-bf19-3751ce4e6a1d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"535b99f0-d38e-4b9c-bf19-3751ce4e6a1d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"535b99f0-d38e-4b9c-bf19-3751ce4e6a1d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"nikolay.nikolov@jiminny.com\"} {\"correlation_id\":\"535b99f0-d38e-4b9c-bf19-3751ce4e6a1d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"nikolay.nikolov@jiminny.com\"} {\"correlation_id\":\"535b99f0-d38e-4b9c-bf19-3751ce4e6a1d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"nikolay.nikolov@jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"535b99f0-d38e-4b9c-bf19-3751ce4e6a1d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":614381,\"team_id\":2,\"email\":\"nikolay.nikolov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"535b99f0-d38e-4b9c-bf19-3751ce4e6a1d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":614381,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"535b99f0-d38e-4b9c-bf19-3751ce4e6a1d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":614381} {\"correlation_id\":\"535b99f0-d38e-4b9c-bf19-3751ce4e6a1d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":614381,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"535b99f0-d38e-4b9c-bf19-3751ce4e6a1d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":614381,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"535b99f0-d38e-4b9c-bf19-3751ce4e6a1d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":614378,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":6167,\"account_id\":null,\"opportunity_id\":null,\"stage_id\":null}} {\"correlation_id\":\"f934b469-16df-4a4b-b5cc-bf918da87b87\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":614378} {\"correlation_id\":\"f934b469-16df-4a4b-b5cc-bf918da87b87\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":614378,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"f934b469-16df-4a4b-b5cc-bf918da87b87\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":614378,\"participants\":[{\"id\":1002623,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":1002624,\"user_id\":null,\"contact_id\":6167,\"lead_id\":null},{\"id\":1002625,\"user_id\":89,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"f934b469-16df-4a4b-b5cc-bf918da87b87\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"f934b469-16df-4a4b-b5cc-bf918da87b87\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"f934b469-16df-4a4b-b5cc-bf918da87b87\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"f934b469-16df-4a4b-b5cc-bf918da87b87\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"f934b469-16df-4a4b-b5cc-bf918da87b87\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"nikolay.nikolov@jiminny.com\"} {\"correlation_id\":\"f934b469-16df-4a4b-b5cc-bf918da87b87\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"nikolay.nikolov@jiminny.com\"} {\"correlation_id\":\"f934b469-16df-4a4b-b5cc-bf918da87b87\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"nikolay.nikolov@jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"f934b469-16df-4a4b-b5cc-bf918da87b87\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"nmalchev@gmail.com\"} {\"correlation_id\":\"f934b469-16df-4a4b-b5cc-bf918da87b87\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":614378,\"team_id\":2,\"email\":\"nikolay.nikolov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"f934b469-16df-4a4b-b5cc-bf918da87b87\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":614378,\"participants_processed\":3,\"exact_matches\":1,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"f934b469-16df-4a4b-b5cc-bf918da87b87\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":614378} {\"correlation_id\":\"f934b469-16df-4a4b-b5cc-bf918da87b87\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":614378,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"f934b469-16df-4a4b-b5cc-bf918da87b87\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":614378,\"remote_search\":true,\"lead_id\":null,\"contact_id\":6167,\"account_id\":null,\"opportunity_id\":null,\"stage_id\":null} {\"correlation_id\":\"f934b469-16df-4a4b-b5cc-bf918da87b87\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":613840,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36}} {\"correlation_id\":\"b440c986-63bd-445c-adfc-4d111801e63c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613840} {\"correlation_id\":\"b440c986-63bd-445c-adfc-4d111801e63c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613840,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"b440c986-63bd-445c-adfc-4d111801e63c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":613840,\"participants\":[{\"id\":1001764,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null},{\"id\":1001765,\"user_id\":261,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"b440c986-63bd-445c-adfc-4d111801e63c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"b440c986-63bd-445c-adfc-4d111801e63c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"b440c986-63bd-445c-adfc-4d111801e63c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"b440c986-63bd-445c-adfc-4d111801e63c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"b440c986-63bd-445c-adfc-4d111801e63c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: ProspectCache - Searching DB for opportunity by owner {\"account_id\":244,\"contact_id\":4487,\"owner_id\":261} {\"correlation_id\":\"b440c986-63bd-445c-adfc-4d111801e63c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: ProspectCache - Opportunity DB search results {\"account_id\":244,\"contact_id\":4487,\"opportunity_id\":299} {\"correlation_id\":\"b440c986-63bd-445c-adfc-4d111801e63c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"b440c986-63bd-445c-adfc-4d111801e63c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":613840,\"team_id\":2,\"email\":\"adelina.petrova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"b440c986-63bd-445c-adfc-4d111801e63c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":613840,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"b440c986-63bd-445c-adfc-4d111801e63c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613840} {\"correlation_id\":\"b440c986-63bd-445c-adfc-4d111801e63c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613840,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"b440c986-63bd-445c-adfc-4d111801e63c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":613840,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36} {\"correlation_id\":\"b440c986-63bd-445c-adfc-4d111801e63c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":613833,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36}} {\"correlation_id\":\"0360a22a-94dd-43cb-b57b-06bc159a05ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613833} {\"correlation_id\":\"0360a22a-94dd-43cb-b57b-06bc159a05ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613833,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"0360a22a-94dd-43cb-b57b-06bc159a05ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":613833,\"participants\":[{\"id\":1001750,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null},{\"id\":1001751,\"user_id\":261,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"0360a22a-94dd-43cb-b57b-06bc159a05ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"0360a22a-94dd-43cb-b57b-06bc159a05ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"0360a22a-94dd-43cb-b57b-06bc159a05ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"0360a22a-94dd-43cb-b57b-06bc159a05ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"0360a22a-94dd-43cb-b57b-06bc159a05ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"0360a22a-94dd-43cb-b57b-06bc159a05ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":613833,\"team_id\":2,\"email\":\"adelina.petrova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"0360a22a-94dd-43cb-b57b-06bc159a05ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":613833,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"0360a22a-94dd-43cb-b57b-06bc159a05ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613833} {\"correlation_id\":\"0360a22a-94dd-43cb-b57b-06bc159a05ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613833,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"0360a22a-94dd-43cb-b57b-06bc159a05ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":613833,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36} {\"correlation_id\":\"0360a22a-94dd-43cb-b57b-06bc159a05ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":613827,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36}} {\"correlation_id\":\"32fbe130-bf29-4e37-9c5a-bbe81f416a36\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613827} {\"correlation_id\":\"32fbe130-bf29-4e37-9c5a-bbe81f416a36\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613827,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"32fbe130-bf29-4e37-9c5a-bbe81f416a36\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":613827,\"participants\":[{\"id\":1001734,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null},{\"id\":1001735,\"user_id\":261,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"32fbe130-bf29-4e37-9c5a-bbe81f416a36\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"32fbe130-bf29-4e37-9c5a-bbe81f416a36\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"32fbe130-bf29-4e37-9c5a-bbe81f416a36\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"32fbe130-bf29-4e37-9c5a-bbe81f416a36\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"32fbe130-bf29-4e37-9c5a-bbe81f416a36\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"32fbe130-bf29-4e37-9c5a-bbe81f416a36\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":613827,\"team_id\":2,\"email\":\"adelina.petrova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"32fbe130-bf29-4e37-9c5a-bbe81f416a36\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":613827,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"32fbe130-bf29-4e37-9c5a-bbe81f416a36\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613827} {\"correlation_id\":\"32fbe130-bf29-4e37-9c5a-bbe81f416a36\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613827,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"32fbe130-bf29-4e37-9c5a-bbe81f416a36\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":613827,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36} {\"correlation_id\":\"32fbe130-bf29-4e37-9c5a-bbe81f416a36\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":613826,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36}} {\"correlation_id\":\"9b9716ff-ede9-4377-8ee8-b3c9a68b2dfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613826} {\"correlation_id\":\"9b9716ff-ede9-4377-8ee8-b3c9a68b2dfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613826,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"9b9716ff-ede9-4377-8ee8-b3c9a68b2dfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":613826,\"participants\":[{\"id\":1001732,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null},{\"id\":1001733,\"user_id\":261,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"9b9716ff-ede9-4377-8ee8-b3c9a68b2dfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"9b9716ff-ede9-4377-8ee8-b3c9a68b2dfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"9b9716ff-ede9-4377-8ee8-b3c9a68b2dfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"9b9716ff-ede9-4377-8ee8-b3c9a68b2dfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"9b9716ff-ede9-4377-8ee8-b3c9a68b2dfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"9b9716ff-ede9-4377-8ee8-b3c9a68b2dfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":613826,\"team_id\":2,\"email\":\"adelina.petrova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"9b9716ff-ede9-4377-8ee8-b3c9a68b2dfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":613826,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"9b9716ff-ede9-4377-8ee8-b3c9a68b2dfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613826} {\"correlation_id\":\"9b9716ff-ede9-4377-8ee8-b3c9a68b2dfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613826,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"9b9716ff-ede9-4377-8ee8-b3c9a68b2dfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":613826,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36} {\"correlation_id\":\"9b9716ff-ede9-4377-8ee8-b3c9a68b2dfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":613820,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36}} {\"correlation_id\":\"a131075c-6ae7-4912-92e1-adc33c291d1e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613820} {\"correlation_id\":\"a131075c-6ae7-4912-92e1-adc33c291d1e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613820,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"a131075c-6ae7-4912-92e1-adc33c291d1e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":613820,\"participants\":[{\"id\":1001721,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null},{\"id\":1001722,\"user_id\":261,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"a131075c-6ae7-4912-92e1-adc33c291d1e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"a131075c-6ae7-4912-92e1-adc33c291d1e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"a131075c-6ae7-4912-92e1-adc33c291d1e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"a131075c-6ae7-4912-92e1-adc33c291d1e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"a131075c-6ae7-4912-92e1-adc33c291d1e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"a131075c-6ae7-4912-92e1-adc33c291d1e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":613820,\"team_id\":2,\"email\":\"adelina.petrova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"a131075c-6ae7-4912-92e1-adc33c291d1e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":613820,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"a131075c-6ae7-4912-92e1-adc33c291d1e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613820} {\"correlation_id\":\"a131075c-6ae7-4912-92e1-adc33c291d1e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613820,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"a131075c-6ae7-4912-92e1-adc33c291d1e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":613820,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36} {\"correlation_id\":\"a131075c-6ae7-4912-92e1-adc33c291d1e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":613818,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36}} {\"correlation_id\":\"d36e09b8-5b15-40f2-8fa0-0beb3f0ebe69\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613818} {\"correlation_id\":\"d36e09b8-5b15-40f2-8fa0-0beb3f0ebe69\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613818,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"d36e09b8-5b15-40f2-8fa0-0beb3f0ebe69\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":613818,\"participants\":[{\"id\":1001717,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null},{\"id\":1001718,\"user_id\":261,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"d36e09b8-5b15-40f2-8fa0-0beb3f0ebe69\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"d36e09b8-5b15-40f2-8fa0-0beb3f0ebe69\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"d36e09b8-5b15-40f2-8fa0-0beb3f0ebe69\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d36e09b8-5b15-40f2-8fa0-0beb3f0ebe69\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"d36e09b8-5b15-40f2-8fa0-0beb3f0ebe69\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"d36e09b8-5b15-40f2-8fa0-0beb3f0ebe69\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":613818,\"team_id\":2,\"email\":\"adelina.petrova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"d36e09b8-5b15-40f2-8fa0-0beb3f0ebe69\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":613818,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"d36e09b8-5b15-40f2-8fa0-0beb3f0ebe69\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613818} {\"correlation_id\":\"d36e09b8-5b15-40f2-8fa0-0beb3f0ebe69\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613818,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"d36e09b8-5b15-40f2-8fa0-0beb3f0ebe69\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":613818,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36} {\"correlation_id\":\"d36e09b8-5b15-40f2-8fa0-0beb3f0ebe69\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":613812,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36}} {\"correlation_id\":\"9e5ea20b-def9-4ace-8a73-9d08a04cc6d9\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613812} {\"correlation_id\":\"9e5ea20b-def9-4ace-8a73-9d08a04cc6d9\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613812,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"9e5ea20b-def9-4ace-8a73-9d08a04cc6d9\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":613812,\"participants\":[{\"id\":1001705,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null},{\"id\":1001706,\"user_id\":261,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"9e5ea20b-def9-4ace-8a73-9d08a04cc6d9\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"9e5ea20b-def9-4ace-8a73-9d08a04cc6d9\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"9e5ea20b-def9-4ace-8a73-9d08a04cc6d9\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"9e5ea20b-def9-4ace-8a73-9d08a04cc6d9\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"9e5ea20b-def9-4ace-8a73-9d08a04cc6d9\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"9e5ea20b-def9-4ace-8a73-9d08a04cc6d9\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":613812,\"team_id\":2,\"email\":\"adelina.petrova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"9e5ea20b-def9-4ace-8a73-9d08a04cc6d9\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":613812,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"9e5ea20b-def9-4ace-8a73-9d08a04cc6d9\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613812} {\"correlation_id\":\"9e5ea20b-def9-4ace-8a73-9d08a04cc6d9\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613812,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"9e5ea20b-def9-4ace-8a73-9d08a04cc6d9\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":613812,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36} {\"correlation_id\":\"9e5ea20b-def9-4ace-8a73-9d08a04cc6d9\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":613807,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4484,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"be53e876-a319-40dc-a89b-0728d9a47c6e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613807} {\"correlation_id\":\"be53e876-a319-40dc-a89b-0728d9a47c6e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613807,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"be53e876-a319-40dc-a89b-0728d9a47c6e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":613807,\"participants\":[{\"id\":1001690,\"user_id\":253,\"contact_id\":null,\"lead_id\":null},{\"id\":1001691,\"user_id\":null,\"contact_id\":4484,\"lead_id\":null}]} {\"correlation_id\":\"be53e876-a319-40dc-a89b-0728d9a47c6e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"be53e876-a319-40dc-a89b-0728d9a47c6e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"be53e876-a319-40dc-a89b-0728d9a47c6e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"be53e876-a319-40dc-a89b-0728d9a47c6e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"be53e876-a319-40dc-a89b-0728d9a47c6e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":613807,\"team_id\":2,\"email\":\"preslava.ivanova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"be53e876-a319-40dc-a89b-0728d9a47c6e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: ProspectCache - Searching DB for opportunity by owner {\"account_id\":243,\"contact_id\":4484,\"owner_id\":253} {\"correlation_id\":\"be53e876-a319-40dc-a89b-0728d9a47c6e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: ProspectCache - Fallback DB opportunity search {\"account_id\":243,\"contact_id\":4484} {\"correlation_id\":\"be53e876-a319-40dc-a89b-0728d9a47c6e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: ProspectCache - Opportunity DB search results {\"account_id\":243,\"contact_id\":4484,\"opportunity_id\":276} {\"correlation_id\":\"be53e876-a319-40dc-a89b-0728d9a47c6e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"tsvetomir.banovski@gmail.com\"} {\"correlation_id\":\"be53e876-a319-40dc-a89b-0728d9a47c6e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":613807,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"be53e876-a319-40dc-a89b-0728d9a47c6e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613807} {\"correlation_id\":\"be53e876-a319-40dc-a89b-0728d9a47c6e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613807,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"be53e876-a319-40dc-a89b-0728d9a47c6e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":613807,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4484,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"be53e876-a319-40dc-a89b-0728d9a47c6e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":613806,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":350,\"stage_id\":34}} {\"correlation_id\":\"94c53200-4779-41ab-82ea-571c1bcdd8b7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613806} {\"correlation_id\":\"94c53200-4779-41ab-82ea-571c1bcdd8b7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613806,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"94c53200-4779-41ab-82ea-571c1bcdd8b7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":613806,\"participants\":[{\"id\":1001688,\"user_id\":253,\"contact_id\":null,\"lead_id\":null},{\"id\":1001689,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null}]} {\"correlation_id\":\"94c53200-4779-41ab-82ea-571c1bcdd8b7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"94c53200-4779-41ab-82ea-571c1bcdd8b7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"94c53200-4779-41ab-82ea-571c1bcdd8b7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"94c53200-4779-41ab-82ea-571c1bcdd8b7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"94c53200-4779-41ab-82ea-571c1bcdd8b7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":613806,\"team_id\":2,\"email\":\"preslava.ivanova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"94c53200-4779-41ab-82ea-571c1bcdd8b7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: ProspectCache - Searching DB for opportunity by owner {\"account_id\":244,\"contact_id\":4487,\"owner_id\":253} {\"correlation_id\":\"94c53200-4779-41ab-82ea-571c1bcdd8b7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: ProspectCache - Fallback DB opportunity search {\"account_id\":244,\"contact_id\":4487} {\"correlation_id\":\"94c53200-4779-41ab-82ea-571c1bcdd8b7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: ProspectCache - Opportunity DB search results {\"account_id\":244,\"contact_id\":4487,\"opportunity_id\":350} {\"correlation_id\":\"94c53200-4779-41ab-82ea-571c1bcdd8b7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"94c53200-4779-41ab-82ea-571c1bcdd8b7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":613806,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"94c53200-4779-41ab-82ea-571c1bcdd8b7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613806} {\"correlation_id\":\"94c53200-4779-41ab-82ea-571c1bcdd8b7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613806,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"94c53200-4779-41ab-82ea-571c1bcdd8b7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":613806,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":350,\"stage_id\":34} {\"correlation_id\":\"94c53200-4779-41ab-82ea-571c1bcdd8b7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":613805,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":350,\"stage_id\":34}} {\"correlation_id\":\"d1a190ef-9b03-49ff-ad47-50afa53ef345\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613805} {\"correlation_id\":\"d1a190ef-9b03-49ff-ad47-50afa53ef345\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613805,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"d1a190ef-9b03-49ff-ad47-50afa53ef345\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":613805,\"participants\":[{\"id\":1001686,\"user_id\":253,\"contact_id\":null,\"lead_id\":null},{\"id\":1001687,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null}]} {\"correlation_id\":\"d1a190ef-9b03-49ff-ad47-50afa53ef345\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"d1a190ef-9b03-49ff-ad47-50afa53ef345\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"d1a190ef-9b03-49ff-ad47-50afa53ef345\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d1a190ef-9b03-49ff-ad47-50afa53ef345\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"d1a190ef-9b03-49ff-ad47-50afa53ef345\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":613805,\"team_id\":2,\"email\":\"preslava.ivanova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"d1a190ef-9b03-49ff-ad47-50afa53ef345\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"d1a190ef-9b03-49ff-ad47-50afa53ef345\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":613805,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"d1a190ef-9b03-49ff-ad47-50afa53ef345\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613805} {\"correlation_id\":\"d1a190ef-9b03-49ff-ad47-50afa53ef345\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613805,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"d1a190ef-9b03-49ff-ad47-50afa53ef345\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":613805,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":350,\"stage_id\":34} {\"correlation_id\":\"d1a190ef-9b03-49ff-ad47-50afa53ef345\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":613698,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36}} {\"correlation_id\":\"2fa5fec1-54fb-40cc-93d4-145712a9d2fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613698} {\"correlation_id\":\"2fa5fec1-54fb-40cc-93d4-145712a9d2fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613698,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"2fa5fec1-54fb-40cc-93d4-145712a9d2fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":613698,\"participants\":[{\"id\":1001667,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null},{\"id\":1001668,\"user_id\":261,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"2fa5fec1-54fb-40cc-93d4-145712a9d2fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"2fa5fec1-54fb-40cc-93d4-145712a9d2fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"2fa5fec1-54fb-40cc-93d4-145712a9d2fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"2fa5fec1-54fb-40cc-93d4-145712a9d2fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"2fa5fec1-54fb-40cc-93d4-145712a9d2fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"2fa5fec1-54fb-40cc-93d4-145712a9d2fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":613698,\"team_id\":2,\"email\":\"adelina.petrova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"2fa5fec1-54fb-40cc-93d4-145712a9d2fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":613698,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"2fa5fec1-54fb-40cc-93d4-145712a9d2fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613698} {\"correlation_id\":\"2fa5fec1-54fb-40cc-93d4-145712a9d2fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613698,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"2fa5fec1-54fb-40cc-93d4-145712a9d2fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":613698,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36} {\"correlation_id\":\"2fa5fec1-54fb-40cc-93d4-145712a9d2fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":613697,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36}} {\"correlation_id\":\"4da249c2-1056-47f9-869a-53c5fda5b9ef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613697} {\"correlation_id\":\"4da249c2-1056-47f9-869a-53c5fda5b9ef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613697,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"4da249c2-1056-47f9-869a-53c5fda5b9ef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":613697,\"participants\":[{\"id\":1001665,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null},{\"id\":1001666,\"user_id\":261,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"4da249c2-1056-47f9-869a-53c5fda5b9ef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"4da249c2-1056-47f9-869a-53c5fda5b9ef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"4da249c2-1056-47f9-869a-53c5fda5b9ef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4da249c2-1056-47f9-869a-53c5fda5b9ef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"4da249c2-1056-47f9-869a-53c5fda5b9ef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"4da249c2-1056-47f9-869a-53c5fda5b9ef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":613697,\"team_id\":2,\"email\":\"adelina.petrova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"4da249c2-1056-47f9-869a-53c5fda5b9ef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":613697,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"4da249c2-1056-47f9-869a-53c5fda5b9ef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613697} {\"correlation_id\":\"4da249c2-1056-47f9-869a-53c5fda5b9ef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613697,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"4da249c2-1056-47f9-869a-53c5fda5b9ef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":613697,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36} {\"correlation_id\":\"4da249c2-1056-47f9-869a-53c5fda5b9ef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":613696,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36}} {\"correlation_id\":\"8224abca-8e96-4a39-8baa-b4bc4f746e1b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613696} {\"correlation_id\":\"8224abca-8e96-4a39-8baa-b4bc4f746e1b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613696,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"8224abca-8e96-4a39-8baa-b4bc4f746e1b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":613696,\"participants\":[{\"id\":1001663,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null},{\"id\":1001664,\"user_id\":261,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"8224abca-8e96-4a39-8baa-b4bc4f746e1b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"8224abca-8e96-4a39-8baa-b4bc4f746e1b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"8224abca-8e96-4a39-8baa-b4bc4f746e1b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"8224abca-8e96-4a39-8baa-b4bc4f746e1b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"8224abca-8e96-4a39-8baa-b4bc4f746e1b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"8224abca-8e96-4a39-8baa-b4bc4f746e1b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":613696,\"team_id\":2,\"email\":\"adelina.petrova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"8224abca-8e96-4a39-8baa-b4bc4f746e1b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":613696,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"8224abca-8e96-4a39-8baa-b4bc4f746e1b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613696} {\"correlation_id\":\"8224abca-8e96-4a39-8baa-b4bc4f746e1b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613696,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"8224abca-8e96-4a39-8baa-b4bc4f746e1b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":613696,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36} {\"correlation_id\":\"8224abca-8e96-4a39-8baa-b4bc4f746e1b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":613695,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36}} {\"correlation_id\":\"e6c53892-ee7f-42df-ae80-06e4cab7eeea\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613695} {\"correlation_id\":\"e6c53892-ee7f-42df-ae80-06e4cab7eeea\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613695,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"e6c53892-ee7f-42df-ae80-06e4cab7eeea\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":613695,\"participants\":[{\"id\":1001661,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null},{\"id\":1001662,\"user_id\":261,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"e6c53892-ee7f-42df-ae80-06e4cab7eeea\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"e6c53892-ee7f-42df-ae80-06e4cab7eeea\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"e6c53892-ee7f-42df-ae80-06e4cab7eeea\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"e6c53892-ee7f-42df-ae80-06e4cab7eeea\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"e6c53892-ee7f-42df-ae80-06e4cab7eeea\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"e6c53892-ee7f-42df-ae80-06e4cab7eeea\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":613695,\"team_id\":2,\"email\":\"adelina.petrova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"e6c53892-ee7f-42df-ae80-06e4cab7eeea\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":613695,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"e6c53892-ee7f-42df-ae80-06e4cab7eeea\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613695} {\"correlation_id\":\"e6c53892-ee7f-42df-ae80-06e4cab7eeea\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613695,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"e6c53892-ee7f-42df-ae80-06e4cab7eeea\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":613695,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36} {\"correlation_id\":\"e6c53892-ee7f-42df-ae80-06e4cab7eeea\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.NOTICE: Monitoring start {\"correlation_id\":\"2766b9f0-0c60-4d94-9b69-4f2dc977fe59\",\"trace_id\":\"0810b004-42ab-49a4-b3bf-4fbda4a819fe\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":613694,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36}} {\"correlation_id\":\"34f7f8f5-b1b3-4176-bc4a-f96265db81c5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613694} {\"correlation_id\":\"34f7f8f5-b1b3-4176-bc4a-f96265db81c5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613694,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"34f7f8f5-b1b3-4176-bc4a-f96265db81c5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.NOTICE: Monitoring end {\"correlation_id\":\"2766b9f0-0c60-4d94-9b69-4f2dc977fe59\",\"trace_id\":\"0810b004-42ab-49a4-b3bf-4fbda4a819fe\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":613694,\"participants\":[{\"id\":1001659,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null},{\"id\":1001660,\"user_id\":261,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"34f7f8f5-b1b3-4176-bc4a-f96265db81c5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"34f7f8f5-b1b3-4176-bc4a-f96265db81c5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"34f7f8f5-b1b3-4176-bc4a-f96265db81c5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"34f7f8f5-b1b3-4176-bc4a-f96265db81c5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"34f7f8f5-b1b3-4176-bc4a-f96265db81c5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"34f7f8f5-b1b3-4176-bc4a-f96265db81c5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":613694,\"team_id\":2,\"email\":\"adelina.petrova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"34f7f8f5-b1b3-4176-bc4a-f96265db81c5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":613694,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"34f7f8f5-b1b3-4176-bc4a-f96265db81c5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613694} {\"correlation_id\":\"34f7f8f5-b1b3-4176-bc4a-f96265db81c5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613694,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"34f7f8f5-b1b3-4176-bc4a-f96265db81c5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":613694,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36} {\"correlation_id\":\"34f7f8f5-b1b3-4176-bc4a-f96265db81c5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":613157,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":350,\"stage_id\":34}} {\"correlation_id\":\"38fd2794-89d3-4ce9-a9fc-f9f8382e30d5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613157} {\"correlation_id\":\"38fd2794-89d3-4ce9-a9fc-f9f8382e30d5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613157,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"38fd2794-89d3-4ce9-a9fc-f9f8382e30d5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":613157,\"participants\":[{\"id\":1000746,\"user_id\":253,\"contact_id\":null,\"lead_id\":null},{\"id\":1000747,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null}]} {\"correlation_id\":\"38fd2794-89d3-4ce9-a9fc-f9f8382e30d5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"38fd2794-89d3-4ce9-a9fc-f9f8382e30d5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"38fd2794-89d3-4ce9-a9fc-f9f8382e30d5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"38fd2794-89d3-4ce9-a9fc-f9f8382e30d5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"38fd2794-89d3-4ce9-a9fc-f9f8382e30d5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":613157,\"team_id\":2,\"email\":\"preslava.ivanova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"38fd2794-89d3-4ce9-a9fc-f9f8382e30d5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"38fd2794-89d3-4ce9-a9fc-f9f8382e30d5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":613157,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"38fd2794-89d3-4ce9-a9fc-f9f8382e30d5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613157} {\"correlation_id\":\"38fd2794-89d3-4ce9-a9fc-f9f8382e30d5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613157,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"38fd2794-89d3-4ce9-a9fc-f9f8382e30d5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":613157,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":350,\"stage_id\":34} {\"correlation_id\":\"38fd2794-89d3-4ce9-a9fc-f9f8382e30d5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":613156,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":350,\"stage_id\":34}} {\"correlation_id\":\"f33d171a-96c9-465c-bcf2-facecea0a7d6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613156} {\"correlation_id\":\"f33d171a-96c9-465c-bcf2-facecea0a7d6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613156,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"f33d171a-96c9-465c-bcf2-facecea0a7d6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":613156,\"participants\":[{\"id\":1000744,\"user_id\":253,\"contact_id\":null,\"lead_id\":null},{\"id\":1000745,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null}]} {\"correlation_id\":\"f33d171a-96c9-465c-bcf2-facecea0a7d6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"f33d171a-96c9-465c-bcf2-facecea0a7d6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"f33d171a-96c9-465c-bcf2-facecea0a7d6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"f33d171a-96c9-465c-bcf2-facecea0a7d6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"f33d171a-96c9-465c-bcf2-facecea0a7d6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":613156,\"team_id\":2,\"email\":\"preslava.ivanova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"f33d171a-96c9-465c-bcf2-facecea0a7d6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"f33d171a-96c9-465c-bcf2-facecea0a7d6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":613156,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"f33d171a-96c9-465c-bcf2-facecea0a7d6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613156} {\"correlation_id\":\"f33d171a-96c9-465c-bcf2-facecea0a7d6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613156,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"f33d171a-96c9-465c-bcf2-facecea0a7d6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":613156,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":350,\"stage_id\":34} {\"correlation_id\":\"f33d171a-96c9-465c-bcf2-facecea0a7d6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":613155,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":350,\"stage_id\":34}} {\"correlation_id\":\"6907203d-5426-446b-b0ed-cb3a79828876\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613155} {\"correlation_id\":\"6907203d-5426-446b-b0ed-cb3a79828876\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613155,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"6907203d-5426-446b-b0ed-cb3a79828876\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":613155,\"participants\":[{\"id\":1000742,\"user_id\":253,\"contact_id\":null,\"lead_id\":null},{\"id\":1000743,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null}]} {\"correlation_id\":\"6907203d-5426-446b-b0ed-cb3a79828876\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"6907203d-5426-446b-b0ed-cb3a79828876\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"6907203d-5426-446b-b0ed-cb3a79828876\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"6907203d-5426-446b-b0ed-cb3a79828876\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"6907203d-5426-446b-b0ed-cb3a79828876\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":613155,\"team_id\":2,\"email\":\"preslava.ivanova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"6907203d-5426-446b-b0ed-cb3a79828876\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"6907203d-5426-446b-b0ed-cb3a79828876\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":613155,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"6907203d-5426-446b-b0ed-cb3a79828876\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613155} {\"correlation_id\":\"6907203d-5426-446b-b0ed-cb3a79828876\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613155,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"6907203d-5426-446b-b0ed-cb3a79828876\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":613155,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":350,\"stage_id\":34} {\"correlation_id\":\"6907203d-5426-446b-b0ed-cb3a79828876\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":613130,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36}} {\"correlation_id\":\"b39eb37f-ecba-45cf-99d3-dc45ee004976\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613130} {\"correlation_id\":\"b39eb37f-ecba-45cf-99d3-dc45ee004976\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613130,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"b39eb37f-ecba-45cf-99d3-dc45ee004976\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":613130,\"participants\":[{\"id\":1000693,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null},{\"id\":1000694,\"user_id\":261,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"b39eb37f-ecba-45cf-99d3-dc45ee004976\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"b39eb37f-ecba-45cf-99d3-dc45ee004976\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"b39eb37f-ecba-45cf-99d3-dc45ee004976\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"b39eb37f-ecba-45cf-99d3-dc45ee004976\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"b39eb37f-ecba-45cf-99d3-dc45ee004976\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"b39eb37f-ecba-45cf-99d3-dc45ee004976\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":613130,\"team_id\":2,\"email\":\"adelina.petrova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"b39eb37f-ecba-45cf-99d3-dc45ee004976\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":613130,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"b39eb37f-ecba-45cf-99d3-dc45ee004976\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613130} {\"correlation_id\":\"b39eb37f-ecba-45cf-99d3-dc45ee004976\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613130,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"b39eb37f-ecba-45cf-99d3-dc45ee004976\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":613130,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36} {\"correlation_id\":\"b39eb37f-ecba-45cf-99d3-dc45ee004976\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612924,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":165,\"stage_id\":89}} {\"correlation_id\":\"22353f3a-e0aa-43eb-9a06-f618d38746e7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612924} {\"correlation_id\":\"22353f3a-e0aa-43eb-9a06-f618d38746e7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612924,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"22353f3a-e0aa-43eb-9a06-f618d38746e7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612924,\"participants\":[{\"id\":1000290,\"user_id\":19,\"contact_id\":null,\"lead_id\":null},{\"id\":1000291,\"user_id\":null,\"contact_id\":97,\"lead_id\":null}]} {\"correlation_id\":\"22353f3a-e0aa-43eb-9a06-f618d38746e7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"22353f3a-e0aa-43eb-9a06-f618d38746e7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"22353f3a-e0aa-43eb-9a06-f618d38746e7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"22353f3a-e0aa-43eb-9a06-f618d38746e7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"22353f3a-e0aa-43eb-9a06-f618d38746e7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612924,\"team_id\":2,\"email\":\"james.graham@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"22353f3a-e0aa-43eb-9a06-f618d38746e7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: ProspectCache - Searching DB for opportunity by owner {\"account_id\":69,\"contact_id\":97,\"owner_id\":19} {\"correlation_id\":\"22353f3a-e0aa-43eb-9a06-f618d38746e7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: ProspectCache - Opportunity DB search results {\"account_id\":69,\"contact_id\":97,\"opportunity_id\":165} {\"correlation_id\":\"22353f3a-e0aa-43eb-9a06-f618d38746e7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinsoncrusoe@test.com\"} {\"correlation_id\":\"22353f3a-e0aa-43eb-9a06-f618d38746e7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612924,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"22353f3a-e0aa-43eb-9a06-f618d38746e7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612924} {\"correlation_id\":\"22353f3a-e0aa-43eb-9a06-f618d38746e7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612924,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"22353f3a-e0aa-43eb-9a06-f618d38746e7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612924,\"remote_search\":true,\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":165,\"stage_id\":89} {\"correlation_id\":\"22353f3a-e0aa-43eb-9a06-f618d38746e7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612923,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":165,\"stage_id\":89}} {\"correlation_id\":\"602cc3c5-932c-4d3d-9370-35217adac6e0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612923} {\"correlation_id\":\"602cc3c5-932c-4d3d-9370-35217adac6e0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612923,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"602cc3c5-932c-4d3d-9370-35217adac6e0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612923,\"participants\":[{\"id\":1000288,\"user_id\":19,\"contact_id\":null,\"lead_id\":null},{\"id\":1000289,\"user_id\":null,\"contact_id\":97,\"lead_id\":null}]} {\"correlation_id\":\"602cc3c5-932c-4d3d-9370-35217adac6e0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"602cc3c5-932c-4d3d-9370-35217adac6e0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"602cc3c5-932c-4d3d-9370-35217adac6e0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"602cc3c5-932c-4d3d-9370-35217adac6e0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"602cc3c5-932c-4d3d-9370-35217adac6e0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612923,\"team_id\":2,\"email\":\"james.graham@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"602cc3c5-932c-4d3d-9370-35217adac6e0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinsoncrusoe@test.com\"} {\"correlation_id\":\"602cc3c5-932c-4d3d-9370-35217adac6e0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612923,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"602cc3c5-932c-4d3d-9370-35217adac6e0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612923} {\"correlation_id\":\"602cc3c5-932c-4d3d-9370-35217adac6e0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612923,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"602cc3c5-932c-4d3d-9370-35217adac6e0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612923,\"remote_search\":true,\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":165,\"stage_id\":89} {\"correlation_id\":\"602cc3c5-932c-4d3d-9370-35217adac6e0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612922,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":165,\"stage_id\":89}} {\"correlation_id\":\"4272a600-7709-4e0b-9ddd-2bf9a93ee051\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612922} {\"correlation_id\":\"4272a600-7709-4e0b-9ddd-2bf9a93ee051\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612922,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"4272a600-7709-4e0b-9ddd-2bf9a93ee051\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612922,\"participants\":[{\"id\":1000286,\"user_id\":19,\"contact_id\":null,\"lead_id\":null},{\"id\":1000287,\"user_id\":null,\"contact_id\":97,\"lead_id\":null}]} {\"correlation_id\":\"4272a600-7709-4e0b-9ddd-2bf9a93ee051\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"4272a600-7709-4e0b-9ddd-2bf9a93ee051\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"4272a600-7709-4e0b-9ddd-2bf9a93ee051\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4272a600-7709-4e0b-9ddd-2bf9a93ee051\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"4272a600-7709-4e0b-9ddd-2bf9a93ee051\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612922,\"team_id\":2,\"email\":\"james.graham@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"4272a600-7709-4e0b-9ddd-2bf9a93ee051\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinsoncrusoe@test.com\"} {\"correlation_id\":\"4272a600-7709-4e0b-9ddd-2bf9a93ee051\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612922,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"4272a600-7709-4e0b-9ddd-2bf9a93ee051\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612922} {\"correlation_id\":\"4272a600-7709-4e0b-9ddd-2bf9a93ee051\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612922,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"4272a600-7709-4e0b-9ddd-2bf9a93ee051\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612922,\"remote_search\":true,\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":165,\"stage_id\":89} {\"correlation_id\":\"4272a600-7709-4e0b-9ddd-2bf9a93ee051\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612847,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36}} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612847} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612847,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612847,\"participants\":[{\"id\":1000130,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":1000131,\"user_id\":261,\"contact_id\":null,\"lead_id\":null},{\"id\":1000151,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null}]} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [Prospect match] Cache miss, calling the API {\"identifier_type\":\"email\",\"identifier\":\"adelina.petrova@jiminny.com\"} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [Hubspot] Failed to fetch contact {\"email\":\"adelina.petrova@jiminny.com\",\"reason\":\"[404] Client error: `GET https://api.hubapi.com/crm/v3/objects/contacts/adelina.petrova%40jiminny.com?properties=email%2Cfirstname%2Clastname%2Ccountry%2Cphone%2Cmobilephone%2Cjobtitle%2Chubspot_owner_id%2Cassociatedcompanyid%2Cphoto&archived=0&idProperty=email` resulted in a `404 Not Found` response\"} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [Prospect match] API returned empty result, caching the miss with empty prospect data {\"identifier_type\":\"email\",\"identifier\":\"adelina.petrova@jiminny.com\"} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:27] local.INFO: [Hubspot] Pagination completed {\"team_id\":2,\"endpoint\":\"https://api.hubapi.com/crm/v3/objects/contact/search\",\"total_requests\":1,\"total_records_fetched\":0,\"total_elapsed_seconds\":0.22,\"average_seconds_per_request\":0.22} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:27] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"adelina.petrova@jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:27] local.INFO: [Prospect match] Cache miss {\"identifier_type\":\"domain\",\"identifier\":\"jiminny.com\",\"crm\":\"hubspot\"} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:27] local.INFO: [Prospect match] Cache miss, calling the API {\"identifier_type\":\"domain\",\"identifier\":\"jiminny.com\"} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:27] local.INFO: [HubSpot] importAccount {\"crm_provider_id\":\"749766179\",\"config_id\":2} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:27] local.INFO: [HubSpot] CRM Search requested {\"request\":{\"filterGroups\":[{\"filters\":[{\"propertyName\":\"associations.company\",\"operator\":\"EQ\",\"value\":\"749766179\"},{\"propertyName\":\"dealstage\",\"operator\":\"NOT_IN\",\"values\":[\"closedwon\",\"4040964\",\"59247967\"]},{\"propertyName\":\"dealstage\",\"operator\":\"NOT_IN\",\"values\":[\"closedlost\",\"4040965\",\"59247968\"]}]}],\"sorts\":[{\"propertyName\":\"modifieddate\",\"direction\":\"DESCENDING\"}],\"properties\":[\"dealname\",\"amount\",\"hubspot_owner_id\",\"pipeline\",\"dealstage\",\"closedate\",\"deal_currency_code\"],\"limit\":200}} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:27] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"6bdeb8f0-c361-4cb3-9fb1-c6bdf466a73e\",\"trace_id\":\"d050352e-9bf5-497a-99d6-e46661a04a04\"}\n[2026-05-11 10:43:27] local.INFO: [Hubspot] Pagination completed {\"team_id\":2,\"endpoint\":\"https://api.hubapi.com/crm/v3/objects/deals/search\",\"total_requests\":1,\"total_records_fetched\":10,\"total_elapsed_seconds\":0.25,\"average_seconds_per_request\":0.25} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:27] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":60.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"6bdeb8f0-c361-4cb3-9fb1-c6bdf466a73e\",\"trace_id\":\"d050352e-9bf5-497a-99d6-e46661a04a04\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612847,\"team_id\":2,\"email\":\"adelina.petrova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612847,\"participants_processed\":3,\"exact_matches\":1,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612847} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612847,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612847,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612822,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36}} {\"correlation_id\":\"a8970854-ad6a-4751-9cc5-ffe651d20f06\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612822} {\"correlation_id\":\"a8970854-ad6a-4751-9cc5-ffe651d20f06\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612822,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"a8970854-ad6a-4751-9cc5-ffe651d20f06\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612822,\"participants\":[{\"id\":1000080,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null},{\"id\":1000081,\"user_id\":261,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"a8970854-ad6a-4751-9cc5-ffe651d20f06\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"a8970854-ad6a-4751-9cc5-ffe651d20f06\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"a8970854-ad6a-4751-9cc5-ffe651d20f06\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"a8970854-ad6a-4751-9cc5-ffe651d20f06\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"a8970854-ad6a-4751-9cc5-ffe651d20f06\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"a8970854-ad6a-4751-9cc5-ffe651d20f06\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612822,\"team_id\":2,\"email\":\"adelina.petrova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"a8970854-ad6a-4751-9cc5-ffe651d20f06\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612822,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"a8970854-ad6a-4751-9cc5-ffe651d20f06\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612822} {\"correlation_id\":\"a8970854-ad6a-4751-9cc5-ffe651d20f06\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612822,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"a8970854-ad6a-4751-9cc5-ffe651d20f06\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612822,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36} {\"correlation_id\":\"a8970854-ad6a-4751-9cc5-ffe651d20f06\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612819,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36}} {\"correlation_id\":\"16e6bc16-5ab8-49ce-b7da-c292b8c94105\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612819} {\"correlation_id\":\"16e6bc16-5ab8-49ce-b7da-c292b8c94105\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612819,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"16e6bc16-5ab8-49ce-b7da-c292b8c94105\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612819,\"participants\":[{\"id\":1000073,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null},{\"id\":1000074,\"user_id\":261,\"contact_id\":null,\"lead_id\":null},{\"id\":1000075,\"user_id\":null,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"16e6bc16-5ab8-49ce-b7da-c292b8c94105\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"16e6bc16-5ab8-49ce-b7da-c292b8c94105\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"16e6bc16-5ab8-49ce-b7da-c292b8c94105\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"16e6bc16-5ab8-49ce-b7da-c292b8c94105\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"16e6bc16-5ab8-49ce-b7da-c292b8c94105\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"16e6bc16-5ab8-49ce-b7da-c292b8c94105\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612819,\"team_id\":2,\"email\":\"adelina.petrova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"16e6bc16-5ab8-49ce-b7da-c292b8c94105\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"adelina.petrova@jiminny.com\"} {\"correlation_id\":\"16e6bc16-5ab8-49ce-b7da-c292b8c94105\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"adelina.petrova@jiminny.com\"} {\"correlation_id\":\"16e6bc16-5ab8-49ce-b7da-c292b8c94105\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"adelina.petrova@jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"16e6bc16-5ab8-49ce-b7da-c292b8c94105\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612819,\"participants_processed\":3,\"exact_matches\":1,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"16e6bc16-5ab8-49ce-b7da-c292b8c94105\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612819} {\"correlation_id\":\"16e6bc16-5ab8-49ce-b7da-c292b8c94105\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612819,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"16e6bc16-5ab8-49ce-b7da-c292b8c94105\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612819,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36} {\"correlation_id\":\"16e6bc16-5ab8-49ce-b7da-c292b8c94105\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612673,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36}} {\"correlation_id\":\"8711a017-62dd-479f-b84e-123d6e3f40be\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612673} {\"correlation_id\":\"8711a017-62dd-479f-b84e-123d6e3f40be\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612673,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"8711a017-62dd-479f-b84e-123d6e3f40be\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612673,\"participants\":[{\"id\":999993,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null},{\"id\":999994,\"user_id\":261,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"8711a017-62dd-479f-b84e-123d6e3f40be\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"8711a017-62dd-479f-b84e-123d6e3f40be\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"8711a017-62dd-479f-b84e-123d6e3f40be\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"8711a017-62dd-479f-b84e-123d6e3f40be\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"8711a017-62dd-479f-b84e-123d6e3f40be\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"8711a017-62dd-479f-b84e-123d6e3f40be\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612673,\"team_id\":2,\"email\":\"adelina.petrova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"8711a017-62dd-479f-b84e-123d6e3f40be\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612673,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"8711a017-62dd-479f-b84e-123d6e3f40be\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612673} {\"correlation_id\":\"8711a017-62dd-479f-b84e-123d6e3f40be\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612673,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"8711a017-62dd-479f-b84e-123d6e3f40be\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612673,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36} {\"correlation_id\":\"8711a017-62dd-479f-b84e-123d6e3f40be\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612642,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36}} {\"correlation_id\":\"aa7b8829-1739-4f9b-aad1-c6868d8679ca\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612642} {\"correlation_id\":\"aa7b8829-1739-4f9b-aad1-c6868d8679ca\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612642,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"aa7b8829-1739-4f9b-aad1-c6868d8679ca\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612642,\"participants\":[{\"id\":999935,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null},{\"id\":999936,\"user_id\":261,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"aa7b8829-1739-4f9b-aad1-c6868d8679ca\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"aa7b8829-1739-4f9b-aad1-c6868d8679ca\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"aa7b8829-1739-4f9b-aad1-c6868d8679ca\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"aa7b8829-1739-4f9b-aad1-c6868d8679ca\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"aa7b8829-1739-4f9b-aad1-c6868d8679ca\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"aa7b8829-1739-4f9b-aad1-c6868d8679ca\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612642,\"team_id\":2,\"email\":\"adelina.petrova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"aa7b8829-1739-4f9b-aad1-c6868d8679ca\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612642,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"aa7b8829-1739-4f9b-aad1-c6868d8679ca\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612642} {\"correlation_id\":\"aa7b8829-1739-4f9b-aad1-c6868d8679ca\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612642,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"aa7b8829-1739-4f9b-aad1-c6868d8679ca\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612642,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36} {\"correlation_id\":\"aa7b8829-1739-4f9b-aad1-c6868d8679ca\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612598,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"2fef3119-c9e0-421d-b77e-3cf1e26b480a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612598} {\"correlation_id\":\"2fef3119-c9e0-421d-b77e-3cf1e26b480a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612598,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"2fef3119-c9e0-421d-b77e-3cf1e26b480a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612598,\"participants\":[{\"id\":999857,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null},{\"id\":999858,\"user_id\":206,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"2fef3119-c9e0-421d-b77e-3cf1e26b480a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"2fef3119-c9e0-421d-b77e-3cf1e26b480a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"2fef3119-c9e0-421d-b77e-3cf1e26b480a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"2fef3119-c9e0-421d-b77e-3cf1e26b480a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"2fef3119-c9e0-421d-b77e-3cf1e26b480a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: ProspectCache - Searching DB for opportunity by owner {\"account_id\":243,\"contact_id\":4491,\"owner_id\":206} {\"correlation_id\":\"2fef3119-c9e0-421d-b77e-3cf1e26b480a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: ProspectCache - Fallback DB opportunity search {\"account_id\":243,\"contact_id\":4491} {\"correlation_id\":\"2fef3119-c9e0-421d-b77e-3cf1e26b480a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: ProspectCache - Opportunity DB search results {\"account_id\":243,\"contact_id\":4491,\"opportunity_id\":276} {\"correlation_id\":\"2fef3119-c9e0-421d-b77e-3cf1e26b480a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"2fef3119-c9e0-421d-b77e-3cf1e26b480a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612598,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"2fef3119-c9e0-421d-b77e-3cf1e26b480a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612598,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"2fef3119-c9e0-421d-b77e-3cf1e26b480a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612598} {\"correlation_id\":\"2fef3119-c9e0-421d-b77e-3cf1e26b480a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612598,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"2fef3119-c9e0-421d-b77e-3cf1e26b480a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612598,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"2fef3119-c9e0-421d-b77e-3cf1e26b480a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612597,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"f4aa8c68-1048-465c-8ab6-e414220b2b3d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612597} {\"correlation_id\":\"f4aa8c68-1048-465c-8ab6-e414220b2b3d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612597,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"f4aa8c68-1048-465c-8ab6-e414220b2b3d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612597,\"participants\":[{\"id\":999855,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999856,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null}]} {\"correlation_id\":\"f4aa8c68-1048-465c-8ab6-e414220b2b3d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"f4aa8c68-1048-465c-8ab6-e414220b2b3d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"f4aa8c68-1048-465c-8ab6-e414220b2b3d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"f4aa8c68-1048-465c-8ab6-e414220b2b3d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"f4aa8c68-1048-465c-8ab6-e414220b2b3d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612597,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"f4aa8c68-1048-465c-8ab6-e414220b2b3d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"f4aa8c68-1048-465c-8ab6-e414220b2b3d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612597,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"f4aa8c68-1048-465c-8ab6-e414220b2b3d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612597} {\"correlation_id\":\"f4aa8c68-1048-465c-8ab6-e414220b2b3d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612597,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"f4aa8c68-1048-465c-8ab6-e414220b2b3d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612597,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"f4aa8c68-1048-465c-8ab6-e414220b2b3d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612596,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"885d60db-2622-4f69-9826-b15464181dcb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612596} {\"correlation_id\":\"885d60db-2622-4f69-9826-b15464181dcb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612596,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"885d60db-2622-4f69-9826-b15464181dcb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612596,\"participants\":[{\"id\":999853,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999854,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null}]} {\"correlation_id\":\"885d60db-2622-4f69-9826-b15464181dcb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"885d60db-2622-4f69-9826-b15464181dcb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"885d60db-2622-4f69-9826-b15464181dcb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"885d60db-2622-4f69-9826-b15464181dcb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"885d60db-2622-4f69-9826-b15464181dcb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612596,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"885d60db-2622-4f69-9826-b15464181dcb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"885d60db-2622-4f69-9826-b15464181dcb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612596,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"885d60db-2622-4f69-9826-b15464181dcb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612596} {\"correlation_id\":\"885d60db-2622-4f69-9826-b15464181dcb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612596,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"885d60db-2622-4f69-9826-b15464181dcb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612596,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"885d60db-2622-4f69-9826-b15464181dcb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612595,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"0bc8a5e9-778b-4d99-bf0c-d903bd129c55\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612595} {\"correlation_id\":\"0bc8a5e9-778b-4d99-bf0c-d903bd129c55\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612595,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"0bc8a5e9-778b-4d99-bf0c-d903bd129c55\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612595,\"participants\":[{\"id\":999851,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null},{\"id\":999852,\"user_id\":206,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"0bc8a5e9-778b-4d99-bf0c-d903bd129c55\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"0bc8a5e9-778b-4d99-bf0c-d903bd129c55\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"0bc8a5e9-778b-4d99-bf0c-d903bd129c55\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"0bc8a5e9-778b-4d99-bf0c-d903bd129c55\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"0bc8a5e9-778b-4d99-bf0c-d903bd129c55\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"0bc8a5e9-778b-4d99-bf0c-d903bd129c55\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612595,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"0bc8a5e9-778b-4d99-bf0c-d903bd129c55\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612595,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"0bc8a5e9-778b-4d99-bf0c-d903bd129c55\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612595} {\"correlation_id\":\"0bc8a5e9-778b-4d99-bf0c-d903bd129c55\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612595,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"0bc8a5e9-778b-4d99-bf0c-d903bd129c55\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612595,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"0bc8a5e9-778b-4d99-bf0c-d903bd129c55\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612594,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"9710b4b5-e8f1-4fbf-b865-162ea340046c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612594} {\"correlation_id\":\"9710b4b5-e8f1-4fbf-b865-162ea340046c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612594,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"9710b4b5-e8f1-4fbf-b865-162ea340046c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612594,\"participants\":[{\"id\":999849,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999850,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null}]} {\"correlation_id\":\"9710b4b5-e8f1-4fbf-b865-162ea340046c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"9710b4b5-e8f1-4fbf-b865-162ea340046c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"9710b4b5-e8f1-4fbf-b865-162ea340046c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"9710b4b5-e8f1-4fbf-b865-162ea340046c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"9710b4b5-e8f1-4fbf-b865-162ea340046c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612594,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"9710b4b5-e8f1-4fbf-b865-162ea340046c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"9710b4b5-e8f1-4fbf-b865-162ea340046c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612594,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"9710b4b5-e8f1-4fbf-b865-162ea340046c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612594} {\"correlation_id\":\"9710b4b5-e8f1-4fbf-b865-162ea340046c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612594,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"9710b4b5-e8f1-4fbf-b865-162ea340046c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612594,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"9710b4b5-e8f1-4fbf-b865-162ea340046c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612593,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"59a8137b-0518-4c91-b63d-c794df57a8c1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612593} {\"correlation_id\":\"59a8137b-0518-4c91-b63d-c794df57a8c1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612593,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"59a8137b-0518-4c91-b63d-c794df57a8c1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612593,\"participants\":[{\"id\":999847,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999848,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null}]} {\"correlation_id\":\"59a8137b-0518-4c91-b63d-c794df57a8c1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"59a8137b-0518-4c91-b63d-c794df57a8c1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"59a8137b-0518-4c91-b63d-c794df57a8c1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"59a8137b-0518-4c91-b63d-c794df57a8c1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"59a8137b-0518-4c91-b63d-c794df57a8c1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612593,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"59a8137b-0518-4c91-b63d-c794df57a8c1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"59a8137b-0518-4c91-b63d-c794df57a8c1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612593,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"59a8137b-0518-4c91-b63d-c794df57a8c1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612593} {\"correlation_id\":\"59a8137b-0518-4c91-b63d-c794df57a8c1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612593,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"59a8137b-0518-4c91-b63d-c794df57a8c1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612593,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"59a8137b-0518-4c91-b63d-c794df57a8c1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612592,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"e1f2c9b0-a985-4277-b0da-5190fb9b25d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612592} {\"correlation_id\":\"e1f2c9b0-a985-4277-b0da-5190fb9b25d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612592,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"e1f2c9b0-a985-4277-b0da-5190fb9b25d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612592,\"participants\":[{\"id\":999845,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null},{\"id\":999846,\"user_id\":206,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"e1f2c9b0-a985-4277-b0da-5190fb9b25d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"e1f2c9b0-a985-4277-b0da-5190fb9b25d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"e1f2c9b0-a985-4277-b0da-5190fb9b25d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"e1f2c9b0-a985-4277-b0da-5190fb9b25d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"e1f2c9b0-a985-4277-b0da-5190fb9b25d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"e1f2c9b0-a985-4277-b0da-5190fb9b25d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612592,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"e1f2c9b0-a985-4277-b0da-5190fb9b25d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612592,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"e1f2c9b0-a985-4277-b0da-5190fb9b25d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612592} {\"correlation_id\":\"e1f2c9b0-a985-4277-b0da-5190fb9b25d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612592,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"e1f2c9b0-a985-4277-b0da-5190fb9b25d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612592,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"e1f2c9b0-a985-4277-b0da-5190fb9b25d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612591,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"8a5f33ea-490a-41e3-b71a-872c52626583\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612591} {\"correlation_id\":\"8a5f33ea-490a-41e3-b71a-872c52626583\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612591,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"8a5f33ea-490a-41e3-b71a-872c52626583\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612591,\"participants\":[{\"id\":999843,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999844,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null}]} {\"correlation_id\":\"8a5f33ea-490a-41e3-b71a-872c52626583\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"8a5f33ea-490a-41e3-b71a-872c52626583\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"8a5f33ea-490a-41e3-b71a-872c52626583\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"8a5f33ea-490a-41e3-b71a-872c52626583\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"8a5f33ea-490a-41e3-b71a-872c52626583\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612591,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"8a5f33ea-490a-41e3-b71a-872c52626583\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"8a5f33ea-490a-41e3-b71a-872c52626583\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612591,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"8a5f33ea-490a-41e3-b71a-872c52626583\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612591} {\"correlation_id\":\"8a5f33ea-490a-41e3-b71a-872c52626583\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612591,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"8a5f33ea-490a-41e3-b71a-872c52626583\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612591,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"8a5f33ea-490a-41e3-b71a-872c52626583\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612590,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"b34fb574-2229-4ff9-b572-7e2f801295a8\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612590} {\"correlation_id\":\"b34fb574-2229-4ff9-b572-7e2f801295a8\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612590,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"b34fb574-2229-4ff9-b572-7e2f801295a8\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612590,\"participants\":[{\"id\":999841,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999842,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null}]} {\"correlation_id\":\"b34fb574-2229-4ff9-b572-7e2f801295a8\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"b34fb574-2229-4ff9-b572-7e2f801295a8\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"b34fb574-2229-4ff9-b572-7e2f801295a8\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"b34fb574-2229-4ff9-b572-7e2f801295a8\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"b34fb574-2229-4ff9-b572-7e2f801295a8\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612590,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"b34fb574-2229-4ff9-b572-7e2f801295a8\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"b34fb574-2229-4ff9-b572-7e2f801295a8\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612590,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"b34fb574-2229-4ff9-b572-7e2f801295a8\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612590} {\"correlation_id\":\"b34fb574-2229-4ff9-b572-7e2f801295a8\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612590,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"b34fb574-2229-4ff9-b572-7e2f801295a8\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612590,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"b34fb574-2229-4ff9-b572-7e2f801295a8\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612589,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"746b5162-98d0-4e35-96ca-f6756e9b2d39\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612589} {\"correlation_id\":\"746b5162-98d0-4e35-96ca-f6756e9b2d39\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612589,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"746b5162-98d0-4e35-96ca-f6756e9b2d39\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612589,\"participants\":[{\"id\":999839,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999840,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null}]} {\"correlation_id\":\"746b5162-98d0-4e35-96ca-f6756e9b2d39\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"746b5162-98d0-4e35-96ca-f6756e9b2d39\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"746b5162-98d0-4e35-96ca-f6756e9b2d39\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"746b5162-98d0-4e35-96ca-f6756e9b2d39\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"746b5162-98d0-4e35-96ca-f6756e9b2d39\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612589,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"746b5162-98d0-4e35-96ca-f6756e9b2d39\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"746b5162-98d0-4e35-96ca-f6756e9b2d39\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612589,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"746b5162-98d0-4e35-96ca-f6756e9b2d39\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612589} {\"correlation_id\":\"746b5162-98d0-4e35-96ca-f6756e9b2d39\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612589,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"746b5162-98d0-4e35-96ca-f6756e9b2d39\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612589,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"746b5162-98d0-4e35-96ca-f6756e9b2d39\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612588,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"601702ec-6d8b-4aa3-b7e2-780e758b68d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612588} {\"correlation_id\":\"601702ec-6d8b-4aa3-b7e2-780e758b68d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612588,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"601702ec-6d8b-4aa3-b7e2-780e758b68d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612588,\"participants\":[{\"id\":999837,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null},{\"id\":999838,\"user_id\":206,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"601702ec-6d8b-4aa3-b7e2-780e758b68d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"601702ec-6d8b-4aa3-b7e2-780e758b68d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"601702ec-6d8b-4aa3-b7e2-780e758b68d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"601702ec-6d8b-4aa3-b7e2-780e758b68d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"601702ec-6d8b-4aa3-b7e2-780e758b68d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"601702ec-6d8b-4aa3-b7e2-780e758b68d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612588,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"601702ec-6d8b-4aa3-b7e2-780e758b68d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612588,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"601702ec-6d8b-4aa3-b7e2-780e758b68d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612588} {\"correlation_id\":\"601702ec-6d8b-4aa3-b7e2-780e758b68d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612588,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"601702ec-6d8b-4aa3-b7e2-780e758b68d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612588,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"601702ec-6d8b-4aa3-b7e2-780e758b68d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612587,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"75f31c21-1a08-4a7a-beef-a6ee82905eb2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612587} {\"correlation_id\":\"75f31c21-1a08-4a7a-beef-a6ee82905eb2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612587,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"75f31c21-1a08-4a7a-beef-a6ee82905eb2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612587,\"participants\":[{\"id\":999835,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999836,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null}]} {\"correlation_id\":\"75f31c21-1a08-4a7a-beef-a6ee82905eb2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"75f31c21-1a08-4a7a-beef-a6ee82905eb2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"75f31c21-1a08-4a7a-beef-a6ee82905eb2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"75f31c21-1a08-4a7a-beef-a6ee82905eb2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"75f31c21-1a08-4a7a-beef-a6ee82905eb2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612587,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"75f31c21-1a08-4a7a-beef-a6ee82905eb2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"75f31c21-1a08-4a7a-beef-a6ee82905eb2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612587,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"75f31c21-1a08-4a7a-beef-a6ee82905eb2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612587} {\"correlation_id\":\"75f31c21-1a08-4a7a-beef-a6ee82905eb2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612587,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"75f31c21-1a08-4a7a-beef-a6ee82905eb2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612587,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"75f31c21-1a08-4a7a-beef-a6ee82905eb2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612586,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"b79deb62-f26f-4831-8192-2fca40cc0445\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612586} {\"correlation_id\":\"b79deb62-f26f-4831-8192-2fca40cc0445\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612586,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"b79deb62-f26f-4831-8192-2fca40cc0445\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612586,\"participants\":[{\"id\":999833,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null},{\"id\":999834,\"user_id\":206,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"b79deb62-f26f-4831-8192-2fca40cc0445\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"b79deb62-f26f-4831-8192-2fca40cc0445\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"b79deb62-f26f-4831-8192-2fca40cc0445\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"b79deb62-f26f-4831-8192-2fca40cc0445\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"b79deb62-f26f-4831-8192-2fca40cc0445\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"b79deb62-f26f-4831-8192-2fca40cc0445\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612586,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"b79deb62-f26f-4831-8192-2fca40cc0445\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612586,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"b79deb62-f26f-4831-8192-2fca40cc0445\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612586} {\"correlation_id\":\"b79deb62-f26f-4831-8192-2fca40cc0445\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612586,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"b79deb62-f26f-4831-8192-2fca40cc0445\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612586,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"b79deb62-f26f-4831-8192-2fca40cc0445\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612585,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"2aa37843-11f3-4f11-a9ef-3277ddd89ec3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612585} {\"correlation_id\":\"2aa37843-11f3-4f11-a9ef-3277ddd89ec3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612585,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"2aa37843-11f3-4f11-a9ef-3277ddd89ec3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612585,\"participants\":[{\"id\":999831,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999832,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null}]} {\"correlation_id\":\"2aa37843-11f3-4f11-a9ef-3277ddd89ec3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"2aa37843-11f3-4f11-a9ef-3277ddd89ec3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"2aa37843-11f3-4f11-a9ef-3277ddd89ec3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"2aa37843-11f3-4f11-a9ef-3277ddd89ec3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"2aa37843-11f3-4f11-a9ef-3277ddd89ec3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612585,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"2aa37843-11f3-4f11-a9ef-3277ddd89ec3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"2aa37843-11f3-4f11-a9ef-3277ddd89ec3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612585,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"2aa37843-11f3-4f11-a9ef-3277ddd89ec3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612585} {\"correlation_id\":\"2aa37843-11f3-4f11-a9ef-3277ddd89ec3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612585,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"2aa37843-11f3-4f11-a9ef-3277ddd89ec3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612585,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"2aa37843-11f3-4f11-a9ef-3277ddd89ec3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612584,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"07bfbf03-ba7c-47ad-8252-88913ec31644\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612584} {\"correlation_id\":\"07bfbf03-ba7c-47ad-8252-88913ec31644\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612584,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"07bfbf03-ba7c-47ad-8252-88913ec31644\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612584,\"participants\":[{\"id\":999829,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999830,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null}]} {\"correlation_id\":\"07bfbf03-ba7c-47ad-8252-88913ec31644\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"07bfbf03-ba7c-47ad-8252-88913ec31644\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"07bfbf03-ba7c-47ad-8252-88913ec31644\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"07bfbf03-ba7c-47ad-8252-88913ec31644\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"07bfbf03-ba7c-47ad-8252-88913ec31644\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612584,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"07bfbf03-ba7c-47ad-8252-88913ec31644\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"07bfbf03-ba7c-47ad-8252-88913ec31644\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612584,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"07bfbf03-ba7c-47ad-8252-88913ec31644\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612584} {\"correlation_id\":\"07bfbf03-ba7c-47ad-8252-88913ec31644\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612584,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"07bfbf03-ba7c-47ad-8252-88913ec31644\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612584,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"07bfbf03-ba7c-47ad-8252-88913ec31644\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612583,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"64e9fc7f-c17e-4af3-b884-54a2c388e8f0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612583} {\"correlation_id\":\"64e9fc7f-c17e-4af3-b884-54a2c388e8f0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612583,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"64e9fc7f-c17e-4af3-b884-54a2c388e8f0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612583,\"participants\":[{\"id\":999827,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null},{\"id\":999828,\"user_id\":206,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"64e9fc7f-c17e-4af3-b884-54a2c388e8f0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"64e9fc7f-c17e-4af3-b884-54a2c388e8f0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"64e9fc7f-c17e-4af3-b884-54a2c388e8f0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"64e9fc7f-c17e-4af3-b884-54a2c388e8f0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"64e9fc7f-c17e-4af3-b884-54a2c388e8f0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"64e9fc7f-c17e-4af3-b884-54a2c388e8f0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612583,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"64e9fc7f-c17e-4af3-b884-54a2c388e8f0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612583,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"64e9fc7f-c17e-4af3-b884-54a2c388e8f0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612583} {\"correlation_id\":\"64e9fc7f-c17e-4af3-b884-54a2c388e8f0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612583,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"64e9fc7f-c17e-4af3-b884-54a2c388e8f0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612583,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"64e9fc7f-c17e-4af3-b884-54a2c388e8f0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612582,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"23188089-fcae-491d-b35d-c20491564cfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612582} {\"correlation_id\":\"23188089-fcae-491d-b35d-c20491564cfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612582,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"23188089-fcae-491d-b35d-c20491564cfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612582,\"participants\":[{\"id\":999825,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999826,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null}]} {\"correlation_id\":\"23188089-fcae-491d-b35d-c20491564cfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"23188089-fcae-491d-b35d-c20491564cfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"23188089-fcae-491d-b35d-c20491564cfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"23188089-fcae-491d-b35d-c20491564cfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"23188089-fcae-491d-b35d-c20491564cfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612582,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"23188089-fcae-491d-b35d-c20491564cfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"23188089-fcae-491d-b35d-c20491564cfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612582,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"23188089-fcae-491d-b35d-c20491564cfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612582} {\"correlation_id\":\"23188089-fcae-491d-b35d-c20491564cfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612582,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"23188089-fcae-491d-b35d-c20491564cfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612582,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"23188089-fcae-491d-b35d-c20491564cfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"9f9cc209-aee5-4d58-b4ad-1455693e83d6\",\"trace_id\":\"f11e2691-d834-4325-91f7-003e08888b34\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612581,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"91305be5-0942-40ad-86c8-c2b2f018ea2b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612581} {\"correlation_id\":\"91305be5-0942-40ad-86c8-c2b2f018ea2b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612581,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"91305be5-0942-40ad-86c8-c2b2f018ea2b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612581,\"participants\":[{\"id\":999823,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null},{\"id\":999824,\"user_id\":206,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"91305be5-0942-40ad-86c8-c2b2f018ea2b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"91305be5-0942-40ad-86c8-c2b2f018ea2b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"91305be5-0942-40ad-86c8-c2b2f018ea2b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"91305be5-0942-40ad-86c8-c2b2f018ea2b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"91305be5-0942-40ad-86c8-c2b2f018ea2b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"91305be5-0942-40ad-86c8-c2b2f018ea2b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612581,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"91305be5-0942-40ad-86c8-c2b2f018ea2b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612581,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"91305be5-0942-40ad-86c8-c2b2f018ea2b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612581} {\"correlation_id\":\"91305be5-0942-40ad-86c8-c2b2f018ea2b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612581,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"91305be5-0942-40ad-86c8-c2b2f018ea2b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [EmailSchedule] STARTING batch process {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"9f9cc209-aee5-4d58-b4ad-1455693e83d6\",\"trace_id\":\"f11e2691-d834-4325-91f7-003e08888b34\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612581,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"91305be5-0942-40ad-86c8-c2b2f018ea2b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612565,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"be6ed1a4-4495-4ec9-932f-da6698c31cec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [EmailSchedule] FINISHED batch process {\"host\":\"docker_lamp_1\",\"processed\":0} {\"correlation_id\":\"9f9cc209-aee5-4d58-b4ad-1455693e83d6\",\"trace_id\":\"f11e2691-d834-4325-91f7-003e08888b34\"}\n[2026-05-11 10:43:29] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":60.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"9f9cc209-aee5-4d58-b4ad-1455693e83d6\",\"trace_id\":\"f11e2691-d834-4325-91f7-003e08888b34\"}\n[2026-05-11 10:43:29] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612565} {\"correlation_id\":\"be6ed1a4-4495-4ec9-932f-da6698c31cec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612565,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"be6ed1a4-4495-4ec9-932f-da6698c31cec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612565,\"participants\":[{\"id\":999789,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999790,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null}]} {\"correlation_id\":\"be6ed1a4-4495-4ec9-932f-da6698c31cec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"be6ed1a4-4495-4ec9-932f-da6698c31cec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"be6ed1a4-4495-4ec9-932f-da6698c31cec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"be6ed1a4-4495-4ec9-932f-da6698c31cec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"be6ed1a4-4495-4ec9-932f-da6698c31cec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612565,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"be6ed1a4-4495-4ec9-932f-da6698c31cec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"be6ed1a4-4495-4ec9-932f-da6698c31cec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612565,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"be6ed1a4-4495-4ec9-932f-da6698c31cec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612565} {\"correlation_id\":\"be6ed1a4-4495-4ec9-932f-da6698c31cec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612565,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"be6ed1a4-4495-4ec9-932f-da6698c31cec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612565,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"be6ed1a4-4495-4ec9-932f-da6698c31cec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612563,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":350,\"stage_id\":34}} {\"correlation_id\":\"77b39c9c-1e21-4c1d-925b-4acce1b204d3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612563} {\"correlation_id\":\"77b39c9c-1e21-4c1d-925b-4acce1b204d3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612563,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"77b39c9c-1e21-4c1d-925b-4acce1b204d3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612563,\"participants\":[{\"id\":999784,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999785,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null}]} {\"correlation_id\":\"77b39c9c-1e21-4c1d-925b-4acce1b204d3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"77b39c9c-1e21-4c1d-925b-4acce1b204d3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"77b39c9c-1e21-4c1d-925b-4acce1b204d3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"77b39c9c-1e21-4c1d-925b-4acce1b204d3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"77b39c9c-1e21-4c1d-925b-4acce1b204d3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612563,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"77b39c9c-1e21-4c1d-925b-4acce1b204d3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: ProspectCache - Searching DB for opportunity by owner {\"account_id\":244,\"contact_id\":4487,\"owner_id\":206} {\"correlation_id\":\"77b39c9c-1e21-4c1d-925b-4acce1b204d3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: ProspectCache - Fallback DB opportunity search {\"account_id\":244,\"contact_id\":4487} {\"correlation_id\":\"77b39c9c-1e21-4c1d-925b-4acce1b204d3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: ProspectCache - Opportunity DB search results {\"account_id\":244,\"contact_id\":4487,\"opportunity_id\":350} {\"correlation_id\":\"77b39c9c-1e21-4c1d-925b-4acce1b204d3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"77b39c9c-1e21-4c1d-925b-4acce1b204d3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:30] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612563,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"77b39c9c-1e21-4c1d-925b-4acce1b204d3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:30] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612563} {\"correlation_id\":\"77b39c9c-1e21-4c1d-925b-4acce1b204d3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:30] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612563,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"77b39c9c-1e21-4c1d-925b-4acce1b204d3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:30] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612563,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":350,\"stage_id\":34} {\"correlation_id\":\"77b39c9c-1e21-4c1d-925b-4acce1b204d3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:30] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612562,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"485e0be1-a971-4cde-9ead-4276768bdf66\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:30] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612562} {\"correlation_id\":\"485e0be1-a971-4cde-9ead-4276768bdf66\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:30] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612562,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"485e0be1-a971-4cde-9ead-4276768bdf66\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:30] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612562,\"participants\":[{\"id\":999782,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":999783,\"user_id\":206,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"485e0be1-a971-4cde-9ead-4276768bdf66\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:30] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"485e0be1-a971-4cde-9ead-4276768bdf66\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:30] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"485e0be1-a971-4cde-9ead-4276768bdf66\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:30] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"485e0be1-a971-4cde-9ead-4276768bdf66\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:30] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"485e0be1-a971-4cde-9ead-4276768bdf66\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:30] local.INFO: [Prospect match] Cache miss, calling the API {\"identifier_type\":\"email\",\"identifier\":\"447782589921@txt.staging.jiminny.com\"} {\"correlation_id\":\"485e0be1-a971-4cde-9ead-4276768bdf66\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:30] local.INFO: [Hubspot] Failed to fetch contact {\"email\":\"447782589921@txt.staging.jiminny.com\",\"reason\":\"[404] Client error: `GET https://api.hubapi.com/crm/v3/objects/contacts/447782589921%40txt.staging.jiminny.com?properties=email%2Cfirstname%2Clastname%2Ccountry%2Cphone%2Cmobilephone%2Cjobtitle%2Chubspot_owner_id%2Cassociatedcompanyid%2Cphoto&archived=0&idProperty=email` resulted in a `404 Not Found` response\"} {\"correlation_id\":\"485e0be1-a971-4cde-9ead-4276768bdf66\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:30] local.INFO: [Prospect match] API returned empty result, caching the miss with empty prospect data {\"identifier_type\":\"email\",\"identifier\":\"447782589921@txt.staging.jiminny.com\"} {\"correlation_id\":\"485e0be1-a971-4cde-9ead-4276768bdf66\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:30] local.INFO: [Hubspot] Pagination completed {\"team_id\":2,\"endpoint\":\"https://api.hubapi.com/crm/v3/objects/contact/search\",\"total_requests\":1,\"total_records_fetched\":0,\"total_elapsed_seconds\":0.22,\"average_seconds_per_request\":0.22} {\"correlation_id\":\"485e0be1-a971-4cde-9ead-4276768bdf66\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:30] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"447782589921@txt.staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"485e0be1-a971-4cde-9ead-4276768bdf66\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:30] local.INFO: [Prospect match] Cache miss {\"identifier_type\":\"domain\",\"identifier\":\"jiminny.com\",\"crm\":\"hubspot\"} {\"correlation_id\":\"485e0be1-a971-4cde-9ead-4276768bdf66\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:30] local.INFO: [Prospect match] Cache miss, calling the API {\"identifier_type\":\"domain\",\"identifier\":\"jiminny.com\"} {\"correlation_id\":\"485e0be1-a971-4cde-9ead-4276768bdf66\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:31] local.INFO: [HubSpot] importAccount {\"crm_provider_id\":\"749766179\",\"config_id\":2} {\"correlation_id\":\"485e0be1-a971-4cde-9ead-4276768bdf66\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:31] local.INFO: [HubSpot] CRM Search requested {\"request\":{\"filterGroups\":[{\"filters\":[{\"propertyName\":\"associations.company\",\"operator\":\"EQ\",\"value\":\"749766179\"},{\"propertyName\":\"dealstage\",\"operator\":\"NOT_IN\",\"values\":[\"closedwon\",\"4040964\",\"59247967\"]},{\"propertyName\":\"dealstage\",\"operator\":\"NOT_IN\",\"values\":[\"closedlost\",\"4040965\",\"59247968\"]}]}],\"sorts\":[{\"propertyName\":\"modifieddate\",\"direction\":\"DESCENDING\"}],\"properties\":[\"dealname\",\"amount\",\"hubspot_owner_id\",\"pipeline\",\"dealstage\",\"closedate\",\"deal_currency_code\"],\"limit\":200}} {\"correlation_id\":\"485e0be1-a971-4cde-9ead-4276768bdf66\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:31] local.INFO: [Hubspot] Pagination completed {\"team_id\":2,\"endpoint\":\"https://api.hubapi.com/crm/v3/objects/deals/search\",\"total_requests\":1,\"total_records_fetched\":10,\"total_elapsed_seconds\":0.3,\"average_seconds_per_request\":0.3} {\"correlation_id\":\"485e0be1-a971-4cde-9ead-4276768bdf66\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612562,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"485e0be1-a971-4cde-9ead-4276768bdf66\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612562,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"485e0be1-a971-4cde-9ead-4276768bdf66\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612562} {\"correlation_id\":\"485e0be1-a971-4cde-9ead-4276768bdf66\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612562,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"485e0be1-a971-4cde-9ead-4276768bdf66\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612562,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"485e0be1-a971-4cde-9ead-4276768bdf66\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612561,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"9fffce69-30ed-4c66-9ee9-e980e776cd57\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612561} {\"correlation_id\":\"9fffce69-30ed-4c66-9ee9-e980e776cd57\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612561,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"9fffce69-30ed-4c66-9ee9-e980e776cd57\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612561,\"participants\":[{\"id\":999780,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999781,\"user_id\":null,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"9fffce69-30ed-4c66-9ee9-e980e776cd57\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"9fffce69-30ed-4c66-9ee9-e980e776cd57\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"9fffce69-30ed-4c66-9ee9-e980e776cd57\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"9fffce69-30ed-4c66-9ee9-e980e776cd57\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"9fffce69-30ed-4c66-9ee9-e980e776cd57\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612561,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"9fffce69-30ed-4c66-9ee9-e980e776cd57\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [Prospect match] Cache miss, calling the API {\"identifier_type\":\"email\",\"identifier\":\"447700174614.447782589921.OeREojLVnk@txt.staging.jiminny.com\"} {\"correlation_id\":\"9fffce69-30ed-4c66-9ee9-e980e776cd57\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [Hubspot] Failed to fetch contact {\"email\":\"447700174614.447782589921.OeREojLVnk@txt.staging.jiminny.com\",\"reason\":\"[404] Client error: `GET https://api.hubapi.com/crm/v3/objects/contacts/447700174614.447782589921.OeREojLVnk%40txt.staging.jiminny.com?properties=email%2Cfirstname%2Clastname%2Ccountry%2Cphone%2Cmobilephone%2Cjobtitle%2Chubspot_owner_id%2Cassociatedcompanyid%2Cphoto&archived=0&idProperty=email` resulted in a `404 Not Found` response\"} {\"correlation_id\":\"9fffce69-30ed-4c66-9ee9-e980e776cd57\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [Prospect match] API returned empty result, caching the miss with empty prospect data {\"identifier_type\":\"email\",\"identifier\":\"447700174614.447782589921.OeREojLVnk@txt.staging.jiminny.com\"} {\"correlation_id\":\"9fffce69-30ed-4c66-9ee9-e980e776cd57\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"447700174614.447782589921.OeREojLVnk@txt.staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"9fffce69-30ed-4c66-9ee9-e980e776cd57\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612561,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"9fffce69-30ed-4c66-9ee9-e980e776cd57\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612561} {\"correlation_id\":\"9fffce69-30ed-4c66-9ee9-e980e776cd57\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612561,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"9fffce69-30ed-4c66-9ee9-e980e776cd57\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612561,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"9fffce69-30ed-4c66-9ee9-e980e776cd57\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612560,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"6a946453-4ffe-4df1-b91d-f73baf850a7a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612560} {\"correlation_id\":\"6a946453-4ffe-4df1-b91d-f73baf850a7a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612560,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"6a946453-4ffe-4df1-b91d-f73baf850a7a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612560,\"participants\":[{\"id\":999778,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":999779,\"user_id\":206,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"6a946453-4ffe-4df1-b91d-f73baf850a7a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"6a946453-4ffe-4df1-b91d-f73baf850a7a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"6a946453-4ffe-4df1-b91d-f73baf850a7a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"6a946453-4ffe-4df1-b91d-f73baf850a7a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"6a946453-4ffe-4df1-b91d-f73baf850a7a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"447782589921@txt.staging.jiminny.com\"} {\"correlation_id\":\"6a946453-4ffe-4df1-b91d-f73baf850a7a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"447782589921@txt.staging.jiminny.com\"} {\"correlation_id\":\"6a946453-4ffe-4df1-b91d-f73baf850a7a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"447782589921@txt.staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"6a946453-4ffe-4df1-b91d-f73baf850a7a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612560,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"6a946453-4ffe-4df1-b91d-f73baf850a7a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612560,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"6a946453-4ffe-4df1-b91d-f73baf850a7a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612560} {\"correlation_id\":\"6a946453-4ffe-4df1-b91d-f73baf850a7a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612560,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"6a946453-4ffe-4df1-b91d-f73baf850a7a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612560,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"6a946453-4ffe-4df1-b91d-f73baf850a7a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612559,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":5011,\"stage_id\":34}} {\"correlation_id\":\"6231af67-0852-4f81-a268-953b6db0b3aa\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612559} {\"correlation_id\":\"6231af67-0852-4f81-a268-953b6db0b3aa\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612559,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"6231af67-0852-4f81-a268-953b6db0b3aa\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612559,\"participants\":[{\"id\":999776,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999777,\"user_id\":null,\"contact_id\":97,\"lead_id\":null}]} {\"correlation_id\":\"6231af67-0852-4f81-a268-953b6db0b3aa\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"6231af67-0852-4f81-a268-953b6db0b3aa\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"6231af67-0852-4f81-a268-953b6db0b3aa\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"6231af67-0852-4f81-a268-953b6db0b3aa\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"6231af67-0852-4f81-a268-953b6db0b3aa\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612559,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"6231af67-0852-4f81-a268-953b6db0b3aa\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: ProspectCache - Searching DB for opportunity by owner {\"account_id\":69,\"contact_id\":97,\"owner_id\":206} {\"correlation_id\":\"6231af67-0852-4f81-a268-953b6db0b3aa\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: ProspectCache - Fallback DB opportunity search {\"account_id\":69,\"contact_id\":97} {\"correlation_id\":\"6231af67-0852-4f81-a268-953b6db0b3aa\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: ProspectCache - Opportunity DB search results {\"account_id\":69,\"contact_id\":97,\"opportunity_id\":5011} {\"correlation_id\":\"6231af67-0852-4f81-a268-953b6db0b3aa\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinsoncrusoe@test.com\"} {\"correlation_id\":\"6231af67-0852-4f81-a268-953b6db0b3aa\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612559,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"6231af67-0852-4f81-a268-953b6db0b3aa\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612559} {\"correlation_id\":\"6231af67-0852-4f81-a268-953b6db0b3aa\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612559,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"6231af67-0852-4f81-a268-953b6db0b3aa\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612559,\"remote_search\":true,\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":5011,\"stage_id\":34} {\"correlation_id\":\"6231af67-0852-4f81-a268-953b6db0b3aa\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612558,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":5011,\"stage_id\":34}} {\"correlation_id\":\"f8c4783d-18f2-45b6-a1b8-e9d80b788104\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612558} {\"correlation_id\":\"f8c4783d-18f2-45b6-a1b8-e9d80b788104\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612558,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"f8c4783d-18f2-45b6-a1b8-e9d80b788104\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612558,\"participants\":[{\"id\":999774,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999775,\"user_id\":null,\"contact_id\":97,\"lead_id\":null}]} {\"correlation_id\":\"f8c4783d-18f2-45b6-a1b8-e9d80b788104\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"f8c4783d-18f2-45b6-a1b8-e9d80b788104\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"f8c4783d-18f2-45b6-a1b8-e9d80b788104\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"f8c4783d-18f2-45b6-a1b8-e9d80b788104\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"f8c4783d-18f2-45b6-a1b8-e9d80b788104\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612558,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"f8c4783d-18f2-45b6-a1b8-e9d80b788104\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinsoncrusoe@test.com\"} {\"correlation_id\":\"f8c4783d-18f2-45b6-a1b8-e9d80b788104\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612558,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"f8c4783d-18f2-45b6-a1b8-e9d80b788104\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612558} {\"correlation_id\":\"f8c4783d-18f2-45b6-a1b8-e9d80b788104\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612558,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"f8c4783d-18f2-45b6-a1b8-e9d80b788104\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612558,\"remote_search\":true,\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":5011,\"stage_id\":34} {\"correlation_id\":\"f8c4783d-18f2-45b6-a1b8-e9d80b788104\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612557,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":5011,\"stage_id\":34}} {\"correlation_id\":\"d087a635-67a6-4fb4-be19-85f309031ebb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612557} {\"correlation_id\":\"d087a635-67a6-4fb4-be19-85f309031ebb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612557,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"d087a635-67a6-4fb4-be19-85f309031ebb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612557,\"participants\":[{\"id\":999772,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999773,\"user_id\":null,\"contact_id\":97,\"lead_id\":null}]} {\"correlation_id\":\"d087a635-67a6-4fb4-be19-85f309031ebb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"d087a635-67a6-4fb4-be19-85f309031ebb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"d087a635-67a6-4fb4-be19-85f309031ebb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d087a635-67a6-4fb4-be19-85f309031ebb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"d087a635-67a6-4fb4-be19-85f309031ebb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612557,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"d087a635-67a6-4fb4-be19-85f309031ebb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinsoncrusoe@test.com\"} {\"correlation_id\":\"d087a635-67a6-4fb4-be19-85f309031ebb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612557,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"d087a635-67a6-4fb4-be19-85f309031ebb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612557} {\"correlation_id\":\"d087a635-67a6-4fb4-be19-85f309031ebb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612557,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"d087a635-67a6-4fb4-be19-85f309031ebb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612557,\"remote_search\":true,\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":5011,\"stage_id\":34} {\"correlation_id\":\"d087a635-67a6-4fb4-be19-85f309031ebb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612556,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":5011,\"stage_id\":34}} {\"correlation_id\":\"462bb4b0-f644-4f69-b4a8-6030370c2202\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612556} {\"correlation_id\":\"462bb4b0-f644-4f69-b4a8-6030370c2202\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612556,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"462bb4b0-f644-4f69-b4a8-6030370c2202\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612556,\"participants\":[{\"id\":999770,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999771,\"user_id\":null,\"contact_id\":97,\"lead_id\":null}]} {\"correlation_id\":\"462bb4b0-f644-4f69-b4a8-6030370c2202\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"462bb4b0-f644-4f69-b4a8-6030370c2202\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"462bb4b0-f644-4f69-b4a8-6030370c2202\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"462bb4b0-f644-4f69-b4a8-6030370c2202\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"462bb4b0-f644-4f69-b4a8-6030370c2202\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612556,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"462bb4b0-f644-4f69-b4a8-6030370c2202\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinsoncrusoe@test.com\"} {\"correlation_id\":\"462bb4b0-f644-4f69-b4a8-6030370c2202\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612556,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"462bb4b0-f644-4f69-b4a8-6030370c2202\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612556} {\"correlation_id\":\"462bb4b0-f644-4f69-b4a8-6030370c2202\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612556,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"462bb4b0-f644-4f69-b4a8-6030370c2202\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612556,\"remote_search\":true,\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":5011,\"stage_id\":34} {\"correlation_id\":\"462bb4b0-f644-4f69-b4a8-6030370c2202\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612555,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":5011,\"stage_id\":34}} {\"correlation_id\":\"988eea33-83d6-41f7-93a3-9737c43ee6a3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612555} {\"correlation_id\":\"988eea33-83d6-41f7-93a3-9737c43ee6a3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [ EsUpdateProcessManager ] Finished updating entities in ES {\"worker\":\"\",\"peak_memory\":\"99.73 MB\",\"elapsed_seconds\":4.12,\"update_target\":\"activities\",\"should_iterate_again\":false} {\"correlation_id\":\"4ad05333-9afb-492e-9f0f-b2909ac45b32\",\"trace_id\":\"3d8feb24-b173-4158-b0a4-4cf33af85066\"}\n[2026-05-11 10:43:33] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612555,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"988eea33-83d6-41f7-93a3-9737c43ee6a3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612555,\"participants\":[{\"id\":999768,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999769,\"user_id\":null,\"contact_id\":97,\"lead_id\":null}]} {\"correlation_id\":\"988eea33-83d6-41f7-93a3-9737c43ee6a3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"988eea33-83d6-41f7-93a3-9737c43ee6a3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"988eea33-83d6-41f7-93a3-9737c43ee6a3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"988eea33-83d6-41f7-93a3-9737c43ee6a3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"988eea33-83d6-41f7-93a3-9737c43ee6a3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612555,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"988eea33-83d6-41f7-93a3-9737c43ee6a3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinsoncrusoe@test.com\"} {\"correlation_id\":\"988eea33-83d6-41f7-93a3-9737c43ee6a3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612555,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"988eea33-83d6-41f7-93a3-9737c43ee6a3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612555} {\"correlation_id\":\"988eea33-83d6-41f7-93a3-9737c43ee6a3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612555,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"988eea33-83d6-41f7-93a3-9737c43ee6a3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612555,\"remote_search\":true,\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":5011,\"stage_id\":34} {\"correlation_id\":\"988eea33-83d6-41f7-93a3-9737c43ee6a3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612554,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":5011,\"stage_id\":34}} {\"correlation_id\":\"7ff43be6-a32a-412c-bb3d-1bd40aa30f65\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612554} {\"correlation_id\":\"7ff43be6-a32a-412c-bb3d-1bd40aa30f65\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612554,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"7ff43be6-a32a-412c-bb3d-1bd40aa30f65\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612554,\"participants\":[{\"id\":999766,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999767,\"user_id\":null,\"contact_id\":97,\"lead_id\":null}]} {\"correlation_id\":\"7ff43be6-a32a-412c-bb3d-1bd40aa30f65\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"7ff43be6-a32a-412c-bb3d-1bd40aa30f65\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"7ff43be6-a32a-412c-bb3d-1bd40aa30f65\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"7ff43be6-a32a-412c-bb3d-1bd40aa30f65\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"7ff43be6-a32a-412c-bb3d-1bd40aa30f65\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612554,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"7ff43be6-a32a-412c-bb3d-1bd40aa30f65\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinsoncrusoe@test.com\"} {\"correlation_id\":\"7ff43be6-a32a-412c-bb3d-1bd40aa30f65\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612554,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"7ff43be6-a32a-412c-bb3d-1bd40aa30f65\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612554} {\"correlation_id\":\"7ff43be6-a32a-412c-bb3d-1bd40aa30f65\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612554,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"7ff43be6-a32a-412c-bb3d-1bd40aa30f65\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612554,\"remote_search\":true,\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":5011,\"stage_id\":34} {\"correlation_id\":\"7ff43be6-a32a-412c-bb3d-1bd40aa30f65\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612553,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":5011,\"stage_id\":34}} {\"correlation_id\":\"3e6b8bf5-c432-4502-a026-aaf235b3b359\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612553} {\"correlation_id\":\"3e6b8bf5-c432-4502-a026-aaf235b3b359\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612553,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"3e6b8bf5-c432-4502-a026-aaf235b3b359\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612553,\"participants\":[{\"id\":999764,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999765,\"user_id\":null,\"contact_id\":97,\"lead_id\":null}]} {\"correlation_id\":\"3e6b8bf5-c432-4502-a026-aaf235b3b359\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"3e6b8bf5-c432-4502-a026-aaf235b3b359\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"3e6b8bf5-c432-4502-a026-aaf235b3b359\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"3e6b8bf5-c432-4502-a026-aaf235b3b359\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"3e6b8bf5-c432-4502-a026-aaf235b3b359\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612553,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"3e6b8bf5-c432-4502-a026-aaf235b3b359\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinsoncrusoe@test.com\"} {\"correlation_id\":\"3e6b8bf5-c432-4502-a026-aaf235b3b359\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612553,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"3e6b8bf5-c432-4502-a026-aaf235b3b359\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612553} {\"correlation_id\":\"3e6b8bf5-c432-4502-a026-aaf235b3b359\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612553,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"3e6b8bf5-c432-4502-a026-aaf235b3b359\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612553,\"remote_search\":true,\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":5011,\"stage_id\":34} {\"correlation_id\":\"3e6b8bf5-c432-4502-a026-aaf235b3b359\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"calendar:sync\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:34] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612552,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":5011,\"stage_id\":34}} {\"correlation_id\":\"1d70d062-f785-4c2c-81be-d992f1ef25c0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612552} {\"correlation_id\":\"1d70d062-f785-4c2c-81be-d992f1ef25c0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:retry-failed\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"c2dd1ea7-a2bd-44ee-95a4-60f88c2fcef7\",\"trace_id\":\"dcda0937-682f-46f0-bdc7-d2054de63c89\"}\n[2026-05-11 10:43:34] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612552,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"1d70d062-f785-4c2c-81be-d992f1ef25c0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612552,\"participants\":[{\"id\":999762,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999763,\"user_id\":null,\"contact_id\":97,\"lead_id\":null}]} {\"correlation_id\":\"1d70d062-f785-4c2c-81be-d992f1ef25c0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.NOTICE: Calendar sync start {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:34] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"1d70d062-f785-4c2c-81be-d992f1ef25c0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"1d70d062-f785-4c2c-81be-d992f1ef25c0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"1d70d062-f785-4c2c-81be-d992f1ef25c0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"1d70d062-f785-4c2c-81be-d992f1ef25c0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612552,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"1d70d062-f785-4c2c-81be-d992f1ef25c0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinsoncrusoe@test.com\"} {\"correlation_id\":\"1d70d062-f785-4c2c-81be-d992f1ef25c0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612552,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"1d70d062-f785-4c2c-81be-d992f1ef25c0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612552} {\"correlation_id\":\"1d70d062-f785-4c2c-81be-d992f1ef25c0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612552,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"1d70d062-f785-4c2c-81be-d992f1ef25c0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612552,\"remote_search\":true,\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":5011,\"stage_id\":34} {\"correlation_id\":\"1d70d062-f785-4c2c-81be-d992f1ef25c0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:retry-failed\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":60.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"c2dd1ea7-a2bd-44ee-95a4-60f88c2fcef7\",\"trace_id\":\"dcda0937-682f-46f0-bdc7-d2054de63c89\"}\n[2026-05-11 10:43:34] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612551,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":5011,\"stage_id\":34}} {\"correlation_id\":\"35f5694e-2521-4089-a5f1-4adc747a40bf\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612551} {\"correlation_id\":\"35f5694e-2521-4089-a5f1-4adc747a40bf\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612551,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"35f5694e-2521-4089-a5f1-4adc747a40bf\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612551,\"participants\":[{\"id\":999760,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999761,\"user_id\":null,\"contact_id\":97,\"lead_id\":null}]} {\"correlation_id\":\"35f5694e-2521-4089-a5f1-4adc747a40bf\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"35f5694e-2521-4089-a5f1-4adc747a40bf\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"35f5694e-2521-4089-a5f1-4adc747a40bf\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"35f5694e-2521-4089-a5f1-4adc747a40bf\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"35f5694e-2521-4089-a5f1-4adc747a40bf\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612551,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"35f5694e-2521-4089-a5f1-4adc747a40bf\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinsoncrusoe@test.com\"} {\"correlation_id\":\"35f5694e-2521-4089-a5f1-4adc747a40bf\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612551,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"35f5694e-2521-4089-a5f1-4adc747a40bf\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612551} {\"correlation_id\":\"35f5694e-2521-4089-a5f1-4adc747a40bf\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612551,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"35f5694e-2521-4089-a5f1-4adc747a40bf\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612551,\"remote_search\":true,\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":5011,\"stage_id\":34} {\"correlation_id\":\"35f5694e-2521-4089-a5f1-4adc747a40bf\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1393,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:34] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1393,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:34] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612550,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":5011,\"stage_id\":34}} {\"correlation_id\":\"046f384b-a938-4138-a9e6-7df0ff6eb90b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:34] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612550} {\"correlation_id\":\"046f384b-a938-4138-a9e6-7df0ff6eb90b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612550,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"046f384b-a938-4138-a9e6-7df0ff6eb90b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1393,\"provider\":\"google\",\"refreshToken\":\"5aa7e2d96b53201cd16fca5d2e4ef3ad03320971fc064781d18aee3ae7b99fbf\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:34] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612550,\"participants\":[{\"id\":999758,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999759,\"user_id\":null,\"contact_id\":97,\"lead_id\":null}]} {\"correlation_id\":\"046f384b-a938-4138-a9e6-7df0ff6eb90b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"046f384b-a938-4138-a9e6-7df0ff6eb90b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"046f384b-a938-4138-a9e6-7df0ff6eb90b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"046f384b-a938-4138-a9e6-7df0ff6eb90b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"046f384b-a938-4138-a9e6-7df0ff6eb90b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612550,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"046f384b-a938-4138-a9e6-7df0ff6eb90b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinsoncrusoe@test.com\"} {\"correlation_id\":\"046f384b-a938-4138-a9e6-7df0ff6eb90b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612550,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"046f384b-a938-4138-a9e6-7df0ff6eb90b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612550} {\"correlation_id\":\"046f384b-a938-4138-a9e6-7df0ff6eb90b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612550,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"046f384b-a938-4138-a9e6-7df0ff6eb90b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612550,\"remote_search\":true,\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":5011,\"stage_id\":34} {\"correlation_id\":\"046f384b-a938-4138-a9e6-7df0ff6eb90b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612549,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":5011,\"stage_id\":34}} {\"correlation_id\":\"1fe4380c-c6ba-40c4-9bdc-65cf0a157cda\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612549} {\"correlation_id\":\"1fe4380c-c6ba-40c4-9bdc-65cf0a157cda\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612549,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"1fe4380c-c6ba-40c4-9bdc-65cf0a157cda\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612549,\"participants\":[{\"id\":999756,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999757,\"user_id\":null,\"contact_id\":97,\"lead_id\":null}]} {\"correlation_id\":\"1fe4380c-c6ba-40c4-9bdc-65cf0a157cda\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1393,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Account has been deleted\"}} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:34] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:34] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1393,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:34] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"1fe4380c-c6ba-40c4-9bdc-65cf0a157cda\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"1fe4380c-c6ba-40c4-9bdc-65cf0a157cda\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"1fe4380c-c6ba-40c4-9bdc-65cf0a157cda\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"1fe4380c-c6ba-40c4-9bdc-65cf0a157cda\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1387,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:34] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1387,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:34] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:34] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612549,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"1fe4380c-c6ba-40c4-9bdc-65cf0a157cda\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1387,\"provider\":\"google\",\"refreshToken\":\"8157ac6de94842937194009e9c50e459253600f799dacf6a40755ffdbeb5bba6\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:34] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinsoncrusoe@test.com\"} {\"correlation_id\":\"1fe4380c-c6ba-40c4-9bdc-65cf0a157cda\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612549,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"1fe4380c-c6ba-40c4-9bdc-65cf0a157cda\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612549} {\"correlation_id\":\"1fe4380c-c6ba-40c4-9bdc-65cf0a157cda\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612549,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"1fe4380c-c6ba-40c4-9bdc-65cf0a157cda\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612549,\"remote_search\":true,\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":5011,\"stage_id\":34} {\"correlation_id\":\"1fe4380c-c6ba-40c4-9bdc-65cf0a157cda\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612365,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"9f080e5f-9e25-468c-9d04-5cdf618dc528\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612365} {\"correlation_id\":\"9f080e5f-9e25-468c-9d04-5cdf618dc528\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612365,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"9f080e5f-9e25-468c-9d04-5cdf618dc528\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612365,\"participants\":[{\"id\":999563,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null},{\"id\":999564,\"user_id\":206,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"9f080e5f-9e25-468c-9d04-5cdf618dc528\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"9f080e5f-9e25-468c-9d04-5cdf618dc528\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"9f080e5f-9e25-468c-9d04-5cdf618dc528\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"9f080e5f-9e25-468c-9d04-5cdf618dc528\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1387,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Account has been deleted\"}} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"9f080e5f-9e25-468c-9d04-5cdf618dc528\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1387,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"9f080e5f-9e25-468c-9d04-5cdf618dc528\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612365,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"9f080e5f-9e25-468c-9d04-5cdf618dc528\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1348,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612365,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"9f080e5f-9e25-468c-9d04-5cdf618dc528\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1348,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612365} {\"correlation_id\":\"9f080e5f-9e25-468c-9d04-5cdf618dc528\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612365,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"9f080e5f-9e25-468c-9d04-5cdf618dc528\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1348,\"provider\":\"google\",\"refreshToken\":\"9e7d13d3032d0cb1b79d8e95aef01383e8e91eb52ff8ee960c8a0b6b95cd8c73\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612365,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"9f080e5f-9e25-468c-9d04-5cdf618dc528\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612360,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"e1af9830-ccba-44db-a1a0-d9ac3ec0da4d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612360} {\"correlation_id\":\"e1af9830-ccba-44db-a1a0-d9ac3ec0da4d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612360,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"e1af9830-ccba-44db-a1a0-d9ac3ec0da4d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612360,\"participants\":[{\"id\":999552,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null},{\"id\":999553,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999565,\"user_id\":null,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"e1af9830-ccba-44db-a1a0-d9ac3ec0da4d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"e1af9830-ccba-44db-a1a0-d9ac3ec0da4d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"e1af9830-ccba-44db-a1a0-d9ac3ec0da4d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"e1af9830-ccba-44db-a1a0-d9ac3ec0da4d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"e1af9830-ccba-44db-a1a0-d9ac3ec0da4d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"e1af9830-ccba-44db-a1a0-d9ac3ec0da4d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612360,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"e1af9830-ccba-44db-a1a0-d9ac3ec0da4d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1348,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Bad Request\"}} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1348,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1361,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1361,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1361,\"provider\":\"google\",\"refreshToken\":\"6c843da199c2b9907445329304fcc4ec5057a4ee748d8299641764395c08e1fd\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1361,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Account has been deleted\"}} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1361,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [Hubspot] Pagination completed {\"team_id\":2,\"endpoint\":\"https://api.hubapi.com/crm/v3/objects/contact/search\",\"total_requests\":1,\"total_records_fetched\":0,\"total_elapsed_seconds\":0.24,\"average_seconds_per_request\":0.24} {\"correlation_id\":\"e1af9830-ccba-44db-a1a0-d9ac3ec0da4d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612360,\"participants_processed\":3,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"e1af9830-ccba-44db-a1a0-d9ac3ec0da4d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1310,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612360} {\"correlation_id\":\"e1af9830-ccba-44db-a1a0-d9ac3ec0da4d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1310,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612360,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"e1af9830-ccba-44db-a1a0-d9ac3ec0da4d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1310,\"provider\":\"google\",\"refreshToken\":\"e34818922c2830a660813a63f6169a4a9a992ae2cccd7dc8dd7796cfdb470ef1\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612360,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"e1af9830-ccba-44db-a1a0-d9ac3ec0da4d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612340,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"eb3baeb0-9f99-4e3d-98f1-fdc0d69023fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612340} {\"correlation_id\":\"eb3baeb0-9f99-4e3d-98f1-fdc0d69023fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612340,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"eb3baeb0-9f99-4e3d-98f1-fdc0d69023fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612340,\"participants\":[{\"id\":999516,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null},{\"id\":999517,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999518,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":999519,\"user_id\":null,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"eb3baeb0-9f99-4e3d-98f1-fdc0d69023fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"eb3baeb0-9f99-4e3d-98f1-fdc0d69023fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"eb3baeb0-9f99-4e3d-98f1-fdc0d69023fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"eb3baeb0-9f99-4e3d-98f1-fdc0d69023fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"eb3baeb0-9f99-4e3d-98f1-fdc0d69023fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"eb3baeb0-9f99-4e3d-98f1-fdc0d69023fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612340,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"eb3baeb0-9f99-4e3d-98f1-fdc0d69023fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1310,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Bad Request\"}} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1310,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1333,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1333,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1333,\"provider\":\"google\",\"refreshToken\":\"6c902986546d8e8da1dc539b046cdc1d458f519acc972e5b5f1d6a1a295165e0\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [Hubspot] Pagination completed {\"team_id\":2,\"endpoint\":\"https://api.hubapi.com/crm/v3/objects/contact/search\",\"total_requests\":1,\"total_records_fetched\":0,\"total_elapsed_seconds\":0.22,\"average_seconds_per_request\":0.22} {\"correlation_id\":\"eb3baeb0-9f99-4e3d-98f1-fdc0d69023fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612340,\"participants_processed\":4,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"eb3baeb0-9f99-4e3d-98f1-fdc0d69023fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612340} {\"correlation_id\":\"eb3baeb0-9f99-4e3d-98f1-fdc0d69023fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612340,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"eb3baeb0-9f99-4e3d-98f1-fdc0d69023fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612340,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"eb3baeb0-9f99-4e3d-98f1-fdc0d69023fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612339,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"68ce2e50-cf66-4aab-9ce0-ab242e6027c4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612339} {\"correlation_id\":\"68ce2e50-cf66-4aab-9ce0-ab242e6027c4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612339,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"68ce2e50-cf66-4aab-9ce0-ab242e6027c4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612339,\"participants\":[{\"id\":999514,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null},{\"id\":999515,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999540,\"user_id\":null,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"68ce2e50-cf66-4aab-9ce0-ab242e6027c4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"68ce2e50-cf66-4aab-9ce0-ab242e6027c4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"68ce2e50-cf66-4aab-9ce0-ab242e6027c4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"68ce2e50-cf66-4aab-9ce0-ab242e6027c4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"68ce2e50-cf66-4aab-9ce0-ab242e6027c4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"68ce2e50-cf66-4aab-9ce0-ab242e6027c4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612339,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"68ce2e50-cf66-4aab-9ce0-ab242e6027c4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612339,\"participants_processed\":3,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"68ce2e50-cf66-4aab-9ce0-ab242e6027c4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1333,\"provider\":\"google\",\"responseBody\":{\"error\":\"unauthorized_client\",\"error_description\":\"Unauthorized\"}} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612339} {\"correlation_id\":\"68ce2e50-cf66-4aab-9ce0-ab242e6027c4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1333,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612339,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"68ce2e50-cf66-4aab-9ce0-ab242e6027c4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612339,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"68ce2e50-cf66-4aab-9ce0-ab242e6027c4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1368,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1368,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1368,\"provider\":\"google\",\"refreshToken\":\"d2f128898ff8543bd16b69cfae37896ab85119b0f5ed2b431d739593bb600333\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612336,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"ffc0ea4a-744d-49a4-a9ad-736fbda03384\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612336} {\"correlation_id\":\"ffc0ea4a-744d-49a4-a9ad-736fbda03384\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612336,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"ffc0ea4a-744d-49a4-a9ad-736fbda03384\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612336,\"participants\":[{\"id\":999508,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null},{\"id\":999509,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999512,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":999513,\"user_id\":null,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"ffc0ea4a-744d-49a4-a9ad-736fbda03384\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"ffc0ea4a-744d-49a4-a9ad-736fbda03384\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"ffc0ea4a-744d-49a4-a9ad-736fbda03384\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"ffc0ea4a-744d-49a4-a9ad-736fbda03384\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"ffc0ea4a-744d-49a4-a9ad-736fbda03384\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"ffc0ea4a-744d-49a4-a9ad-736fbda03384\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612336,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"ffc0ea4a-744d-49a4-a9ad-736fbda03384\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612336,\"participants_processed\":4,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"ffc0ea4a-744d-49a4-a9ad-736fbda03384\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612336} {\"correlation_id\":\"ffc0ea4a-744d-49a4-a9ad-736fbda03384\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612336,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"ffc0ea4a-744d-49a4-a9ad-736fbda03384\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612336,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"ffc0ea4a-744d-49a4-a9ad-736fbda03384\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612183,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36}} {\"correlation_id\":\"48880be8-e032-41b2-948e-c4ec99247548\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612183} {\"correlation_id\":\"48880be8-e032-41b2-948e-c4ec99247548\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612183,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"48880be8-e032-41b2-948e-c4ec99247548\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612183,\"participants\":[{\"id\":999227,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null},{\"id\":999228,\"user_id\":261,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"48880be8-e032-41b2-948e-c4ec99247548\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"48880be8-e032-41b2-948e-c4ec99247548\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"48880be8-e032-41b2-948e-c4ec99247548\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"48880be8-e032-41b2-948e-c4ec99247548\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"48880be8-e032-41b2-948e-c4ec99247548\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"48880be8-e032-41b2-948e-c4ec99247548\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612183,\"team_id\":2,\"email\":\"adelina.petrova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"48880be8-e032-41b2-948e-c4ec99247548\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612183,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"48880be8-e032-41b2-948e-c4ec99247548\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612183} {\"correlation_id\":\"48880be8-e032-41b2-948e-c4ec99247548\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612183,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"48880be8-e032-41b2-948e-c4ec99247548\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612183,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36} {\"correlation_id\":\"48880be8-e032-41b2-948e-c4ec99247548\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612182,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36}} {\"correlation_id\":\"3858f13d-426c-474e-98cf-11576b98e5dd\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612182} {\"correlation_id\":\"3858f13d-426c-474e-98cf-11576b98e5dd\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612182,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"3858f13d-426c-474e-98cf-11576b98e5dd\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612182,\"participants\":[{\"id\":999225,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null},{\"id\":999226,\"user_id\":261,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"3858f13d-426c-474e-98cf-11576b98e5dd\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"3858f13d-426c-474e-98cf-11576b98e5dd\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"3858f13d-426c-474e-98cf-11576b98e5dd\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"3858f13d-426c-474e-98cf-11576b98e5dd\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"3858f13d-426c-474e-98cf-11576b98e5dd\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"3858f13d-426c-474e-98cf-11576b98e5dd\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612182,\"team_id\":2,\"email\":\"adelina.petrova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"3858f13d-426c-474e-98cf-11576b98e5dd\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612182,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"3858f13d-426c-474e-98cf-11576b98e5dd\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612182} {\"correlation_id\":\"3858f13d-426c-474e-98cf-11576b98e5dd\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612182,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"3858f13d-426c-474e-98cf-11576b98e5dd\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612182,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36} {\"correlation_id\":\"3858f13d-426c-474e-98cf-11576b98e5dd\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612181,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36}} {\"correlation_id\":\"33fd8677-950a-4069-9642-511743b8b619\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612181} {\"correlation_id\":\"33fd8677-950a-4069-9642-511743b8b619\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612181,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"33fd8677-950a-4069-9642-511743b8b619\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612181,\"participants\":[{\"id\":999223,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null},{\"id\":999224,\"user_id\":261,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"33fd8677-950a-4069-9642-511743b8b619\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"33fd8677-950a-4069-9642-511743b8b619\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"33fd8677-950a-4069-9642-511743b8b619\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"33fd8677-950a-4069-9642-511743b8b619\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"33fd8677-950a-4069-9642-511743b8b619\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"33fd8677-950a-4069-9642-511743b8b619\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1368,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Bad Request\"}} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612181,\"team_id\":2,\"email\":\"adelina.petrova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"33fd8677-950a-4069-9642-511743b8b619\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1368,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612181,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"33fd8677-950a-4069-9642-511743b8b619\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612181} {\"correlation_id\":\"33fd8677-950a-4069-9642-511743b8b619\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612181,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"33fd8677-950a-4069-9642-511743b8b619\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612181,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36} {\"correlation_id\":\"33fd8677-950a-4069-9642-511743b8b619\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1365,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1365,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1365,\"provider\":\"google\",\"refreshToken\":\"7676e4a9afcd082b413248ab5ec6e487021fec6a9bdf315860a59cefad9caad8\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612180,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36}} {\"correlation_id\":\"dc184f1d-d7e2-4bc1-92a0-c572af69ba86\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612180} {\"correlation_id\":\"dc184f1d-d7e2-4bc1-92a0-c572af69ba86\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612180,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"dc184f1d-d7e2-4bc1-92a0-c572af69ba86\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612180,\"participants\":[{\"id\":999221,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null},{\"id\":999222,\"user_id\":261,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"dc184f1d-d7e2-4bc1-92a0-c572af69ba86\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"dc184f1d-d7e2-4bc1-92a0-c572af69ba86\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"dc184f1d-d7e2-4bc1-92a0-c572af69ba86\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"dc184f1d-d7e2-4bc1-92a0-c572af69ba86\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"dc184f1d-d7e2-4bc1-92a0-c572af69ba86\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"dc184f1d-d7e2-4bc1-92a0-c572af69ba86\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612180,\"team_id\":2,\"email\":\"adelina.petrova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"dc184f1d-d7e2-4bc1-92a0-c572af69ba86\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612180,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"dc184f1d-d7e2-4bc1-92a0-c572af69ba86\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612180} {\"correlation_id\":\"dc184f1d-d7e2-4bc1-92a0-c572af69ba86\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612180,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"dc184f1d-d7e2-4bc1-92a0-c572af69ba86\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612180,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36} {\"correlation_id\":\"dc184f1d-d7e2-4bc1-92a0-c572af69ba86\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":611455,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"8f14fe33-00cc-4910-996e-efa541fc5bb7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":611455} {\"correlation_id\":\"8f14fe33-00cc-4910-996e-efa541fc5bb7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":611455,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"8f14fe33-00cc-4910-996e-efa541fc5bb7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":611455,\"participants\":[{\"id\":997961,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":997962,\"user_id\":1460,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"8f14fe33-00cc-4910-996e-efa541fc5bb7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"8f14fe33-00cc-4910-996e-efa541fc5bb7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"8f14fe33-00cc-4910-996e-efa541fc5bb7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"8f14fe33-00cc-4910-996e-efa541fc5bb7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"8f14fe33-00cc-4910-996e-efa541fc5bb7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [Prospect match] Cache miss, calling the API {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"8f14fe33-00cc-4910-996e-efa541fc5bb7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1365,\"provider\":\"google\",\"responseBody\":{\"error\":\"unauthorized_client\",\"error_description\":\"Unauthorized\"}} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1365,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1364,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1364,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1364,\"provider\":\"google\",\"refreshToken\":\"dd5882ebce76e645292ce33ae74238abbb77c0a4ecc6a2bfe723cad82e72ba8e\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.INFO: [Hubspot] Failed to fetch contact {\"email\":\"support@staging.jiminny.com\",\"reason\":\"[404] Client error: `GET https://api.hubapi.com/crm/v3/objects/contacts/support%40staging.jiminny.com?properties=email%2Cfirstname%2Clastname%2Ccountry%2Cphone%2Cmobilephone%2Cjobtitle%2Chubspot_owner_id%2Cassociatedcompanyid%2Cphoto&archived=0&idProperty=email` resulted in a `404 Not Found` response\"} {\"correlation_id\":\"8f14fe33-00cc-4910-996e-efa541fc5bb7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [Prospect match] API returned empty result, caching the miss with empty prospect data {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"8f14fe33-00cc-4910-996e-efa541fc5bb7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1364,\"provider\":\"google\",\"responseBody\":{\"error\":\"unauthorized_client\",\"error_description\":\"Unauthorized\"}} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1364,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1370,\"provider\":\"office\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1370,\"provider\":\"office\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1370,\"provider\":\"office\",\"refreshToken\":\"b7ee8035306d0043cea6e00e7c4fe14f745e44074a1194db62a31cdf8b70af3e\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.INFO: [Hubspot] Pagination completed {\"team_id\":2,\"endpoint\":\"https://api.hubapi.com/crm/v3/objects/contact/search\",\"total_requests\":1,\"total_records_fetched\":0,\"total_elapsed_seconds\":0.21,\"average_seconds_per_request\":0.21} {\"correlation_id\":\"8f14fe33-00cc-4910-996e-efa541fc5bb7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"8f14fe33-00cc-4910-996e-efa541fc5bb7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [Prospect match] Cache miss {\"identifier_type\":\"domain\",\"identifier\":\"jiminny.com\",\"crm\":\"hubspot\"} {\"correlation_id\":\"8f14fe33-00cc-4910-996e-efa541fc5bb7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [Prospect match] Cache miss, calling the API {\"identifier_type\":\"domain\",\"identifier\":\"jiminny.com\"} {\"correlation_id\":\"8f14fe33-00cc-4910-996e-efa541fc5bb7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1370,\"provider\":\"office\",\"responseBody\":\"{\\\"error\\\":\\\"invalid_client\\\",\\\"error_description\\\":\\\"AADSTS7000215: Invalid client secret provided. Ensure the secret being sent in the request is the client secret value, not the client secret ID, for a secret added to app 'bbcbb2ef-6200-4fae-82bd-d81f5dd738da'. Trace ID: 329b429c-bad8-4f19-bace-40e48a111900 Correlation ID: 4f12c9af-e118-468c-8441-ebc325071d4b Timestamp: 2026-05-11 10:43:36Z\\\",\\\"error_codes\\\":[7000215],\\\"timestamp\\\":\\\"2026-05-11 10:43:36Z\\\",\\\"trace_id\\\":\\\"329b429c-bad8-4f19-bace-40e48a111900\\\",\\\"correlation_id\\\":\\\"4f12c9af-e118-468c-8441-ebc325071d4b\\\",\\\"error_uri\\\":\\\"https://login.microsoftonline.com/error?code=7000215\\\"}\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1370,\"provider\":\"office\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1202,\"provider\":\"office\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1202,\"provider\":\"office\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1202,\"provider\":\"office\",\"refreshToken\":\"b458799ccc29b21a6e2eb5260fdb63e49ccba21bf942a3973fb63799bd7f0afe\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.INFO: [HubSpot] importAccount {\"crm_provider_id\":\"749766179\",\"config_id\":2} {\"correlation_id\":\"8f14fe33-00cc-4910-996e-efa541fc5bb7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [HubSpot] CRM Search requested {\"request\":{\"filterGroups\":[{\"filters\":[{\"propertyName\":\"associations.company\",\"operator\":\"EQ\",\"value\":\"749766179\"},{\"propertyName\":\"dealstage\",\"operator\":\"NOT_IN\",\"values\":[\"closedwon\",\"4040964\",\"59247967\"]},{\"propertyName\":\"dealstage\",\"operator\":\"NOT_IN\",\"values\":[\"closedlost\",\"4040965\",\"59247968\"]}]}],\"sorts\":[{\"propertyName\":\"modifieddate\",\"direction\":\"DESCENDING\"}],\"properties\":[\"dealname\",\"amount\",\"hubspot_owner_id\",\"pipeline\",\"dealstage\",\"closedate\",\"deal_currency_code\"],\"limit\":200}} {\"correlation_id\":\"8f14fe33-00cc-4910-996e-efa541fc5bb7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:37] local.INFO: [Hubspot] Pagination completed {\"team_id\":2,\"endpoint\":\"https://api.hubapi.com/crm/v3/objects/deals/search\",\"total_requests\":1,\"total_records_fetched\":10,\"total_elapsed_seconds\":0.24,\"average_seconds_per_request\":0.24} {\"correlation_id\":\"8f14fe33-00cc-4910-996e-efa541fc5bb7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:37] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":611455,\"team_id\":2,\"email\":\"aneliya.angelova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"8f14fe33-00cc-4910-996e-efa541fc5bb7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:37] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":611455,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"8f14fe33-00cc-4910-996e-efa541fc5bb7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:37] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":611455} {\"correlation_id\":\"8f14fe33-00cc-4910-996e-efa541fc5bb7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:37] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":611455,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"8f14fe33-00cc-4910-996e-efa541fc5bb7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:37] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":611455,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"8f14fe33-00cc-4910-996e-efa541fc5bb7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:37] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":611451,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"7ea49a4a-ec21-4b93-b990-d1e79531b72a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:37] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":611451} {\"correlation_id\":\"7ea49a4a-ec21-4b93-b990-d1e79531b72a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:37] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":611451,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"7ea49a4a-ec21-4b93-b990-d1e79531b72a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:37] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":611451,\"participants\":[{\"id\":997955,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":997956,\"user_id\":18,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"7ea49a4a-ec21-4b93-b990-d1e79531b72a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:37] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"7ea49a4a-ec21-4b93-b990-d1e79531b72a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:37] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"7ea49a4a-ec21-4b93-b990-d1e79531b72a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:37] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"7ea49a4a-ec21-4b93-b990-d1e79531b72a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:37] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"7ea49a4a-ec21-4b93-b990-d1e79531b72a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:37] local.INFO: [Prospect match] Cache miss, calling the API {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"7ea49a4a-ec21-4b93-b990-d1e79531b72a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:38] local.INFO: [Hubspot] Failed to fetch contact {\"email\":\"support@staging.jiminny.com\",\"reason\":\"[404] Client error: `GET https://api.hubapi.com/crm/v3/objects/contacts/support%40staging.jiminny.com?properties=email%2Cfirstname%2Clastname%2Ccountry%2Cphone%2Cmobilephone%2Cjobtitle%2Chubspot_owner_id%2Cassociatedcompanyid%2Cphoto&archived=0&idProperty=email` resulted in a `404 Not Found` response\"} {\"correlation_id\":\"7ea49a4a-ec21-4b93-b990-d1e79531b72a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:38] local.INFO: [Prospect match] API returned empty result, caching the miss with empty prospect data {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"7ea49a4a-ec21-4b93-b990-d1e79531b72a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:38] local.INFO: [Hubspot] Pagination completed {\"team_id\":2,\"endpoint\":\"https://api.hubapi.com/crm/v3/objects/contact/search\",\"total_requests\":1,\"total_records_fetched\":0,\"total_elapsed_seconds\":0.22,\"average_seconds_per_request\":0.22} {\"correlation_id\":\"7ea49a4a-ec21-4b93-b990-d1e79531b72a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:38] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"7ea49a4a-ec21-4b93-b990-d1e79531b72a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:38] local.INFO: [Prospect match] Cache miss {\"identifier_type\":\"domain\",\"identifier\":\"jiminny.com\",\"crm\":\"hubspot\"} {\"correlation_id\":\"7ea49a4a-ec21-4b93-b990-d1e79531b72a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:38] local.INFO: [Prospect match] Cache miss, calling the API {\"identifier_type\":\"domain\",\"identifier\":\"jiminny.com\"} {\"correlation_id\":\"7ea49a4a-ec21-4b93-b990-d1e79531b72a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:38] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1202,\"provider\":\"office\",\"responseBody\":\"{\\\"error\\\":\\\"invalid_client\\\",\\\"error_description\\\":\\\"AADSTS7000215: Invalid client secret provided. Ensure the secret being sent in the request is the client secret value, not the client secret ID, for a secret added to app 'bbcbb2ef-6200-4fae-82bd-d81f5dd738da'. Trace ID: e8a4c6ea-4ab7-4915-b7a3-93868da51100 Correlation ID: e670afc0-d33c-4205-924c-76bda5c3d01d Timestamp: 2026-05-11 10:43:38Z\\\",\\\"error_codes\\\":[7000215],\\\"timestamp\\\":\\\"2026-05-11 10:43:38Z\\\",\\\"trace_id\\\":\\\"e8a4c6ea-4ab7-4915-b7a3-93868da51100\\\",\\\"correlation_id\\\":\\\"e670afc0-d33c-4205-924c-76bda5c3d01d\\\",\\\"error_uri\\\":\\\"https://login.microsoftonline.com/error?code=7000215\\\"}\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:38] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:38] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1202,\"provider\":\"office\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:38] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1502,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:38] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1502,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:38] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:38] local.INFO: Calendar sync job dispatched {\"calendar_id\":501} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:38] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1300,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:38] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1300,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:38] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:38] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1300,\"provider\":\"google\",\"refreshToken\":\"4b811db0725fd9602a95943519a7da935e2a5065da7d9ebfcb170752e3e1ddb8\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:38] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1300,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Account has been deleted\"}} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:38] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:38] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1300,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:38] local.INFO: [HubSpot] importAccount {\"crm_provider_id\":\"749766179\",\"config_id\":2} {\"correlation_id\":\"7ea49a4a-ec21-4b93-b990-d1e79531b72a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:38] local.INFO: [HubSpot] CRM Search requested {\"request\":{\"filterGroups\":[{\"filters\":[{\"propertyName\":\"associations.company\",\"operator\":\"EQ\",\"value\":\"749766179\"},{\"propertyName\":\"dealstage\",\"operator\":\"NOT_IN\",\"values\":[\"closedwon\",\"4040964\",\"59247967\"]},{\"propertyName\":\"dealstage\",\"operator\":\"NOT_IN\",\"values\":[\"closedlost\",\"4040965\",\"59247968\"]}]}],\"sorts\":[{\"propertyName\":\"modifieddate\",\"direction\":\"DESCENDING\"}],\"properties\":[\"dealname\",\"amount\",\"hubspot_owner_id\",\"pipeline\",\"dealstage\",\"closedate\",\"deal_currency_code\"],\"limit\":200}} {\"correlation_id\":\"7ea49a4a-ec21-4b93-b990-d1e79531b72a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:38] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1409,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:38] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1409,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:38] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:38] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1409,\"provider\":\"google\",\"refreshToken\":\"e2a3f2d06894894eed1ee87d9db1ace77d4d42ee6e1288a8940ad2c10333b0c4\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:38] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1409,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Bad Request\"}} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:38] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:38] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1409,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:39] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1352,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:39] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1352,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:39] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:39] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1352,\"provider\":\"google\",\"refreshToken\":\"dd4b16b00fdc1216da6b717c02338c073636e29162826b2de6db3f064fc029eb\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:39] local.INFO: [Hubspot] Pagination completed {\"team_id\":2,\"endpoint\":\"https://api.hubapi.com/crm/v3/objects/deals/search\",\"total_requests\":1,\"total_records_fetched\":10,\"total_elapsed_seconds\":0.28,\"average_seconds_per_request\":0.28} {\"correlation_id\":\"7ea49a4a-ec21-4b93-b990-d1e79531b72a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1352,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Token has been expired or revoked.\"}} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:39] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:39] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1352,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:39] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1296,\"provider\":\"office\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:39] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1296,\"provider\":\"office\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:39] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:39] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1296,\"provider\":\"office\",\"refreshToken\":\"011ae723c9d800c674e0b4be76f49fc046dac7d501b66c59ef0d9549cfa56ae5\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:39] local.INFO: [ EsUpdateProcessManager ] Finished updating entities in ES {\"worker\":\"\",\"peak_memory\":\"99.73 MB\",\"elapsed_seconds\":0.9,\"update_target\":\"activities\",\"should_iterate_again\":false} {\"correlation_id\":\"4ad05333-9afb-492e-9f0f-b2909ac45b32\",\"trace_id\":\"3d8feb24-b173-4158-b0a4-4cf33af85066\"}\n[2026-05-11 10:43:39] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":611451,\"team_id\":2,\"email\":\"veselin.kulov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"7ea49a4a-ec21-4b93-b990-d1e79531b72a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":611451,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"7ea49a4a-ec21-4b93-b990-d1e79531b72a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":611451} {\"correlation_id\":\"7ea49a4a-ec21-4b93-b990-d1e79531b72a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":611451,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"7ea49a4a-ec21-4b93-b990-d1e79531b72a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":611451,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"7ea49a4a-ec21-4b93-b990-d1e79531b72a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":611087,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"02dbf6fa-a774-4090-af49-cabec99363d4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":611087} {\"correlation_id\":\"02dbf6fa-a774-4090-af49-cabec99363d4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":611087,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"02dbf6fa-a774-4090-af49-cabec99363d4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":611087,\"participants\":[{\"id\":997368,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":997369,\"user_id\":18,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"02dbf6fa-a774-4090-af49-cabec99363d4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"02dbf6fa-a774-4090-af49-cabec99363d4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"02dbf6fa-a774-4090-af49-cabec99363d4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"02dbf6fa-a774-4090-af49-cabec99363d4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"02dbf6fa-a774-4090-af49-cabec99363d4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"02dbf6fa-a774-4090-af49-cabec99363d4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"02dbf6fa-a774-4090-af49-cabec99363d4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"02dbf6fa-a774-4090-af49-cabec99363d4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":611087,\"team_id\":2,\"email\":\"veselin.kulov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"02dbf6fa-a774-4090-af49-cabec99363d4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":611087,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"02dbf6fa-a774-4090-af49-cabec99363d4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":611087} {\"correlation_id\":\"02dbf6fa-a774-4090-af49-cabec99363d4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":611087,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"02dbf6fa-a774-4090-af49-cabec99363d4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":611087,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"02dbf6fa-a774-4090-af49-cabec99363d4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1296,\"provider\":\"office\",\"responseBody\":\"{\\\"error\\\":\\\"invalid_client\\\",\\\"error_description\\\":\\\"AADSTS7000215: Invalid client secret provided. Ensure the secret being sent in the request is the client secret value, not the client secret ID, for a secret added to app 'bbcbb2ef-6200-4fae-82bd-d81f5dd738da'. Trace ID: 4ff43bd2-d427-4049-964c-57fbcad91a00 Correlation ID: 6e92626f-a36d-4c41-ae7c-3b7296df0110 Timestamp: 2026-05-11 10:43:39Z\\\",\\\"error_codes\\\":[7000215],\\\"timestamp\\\":\\\"2026-05-11 10:43:39Z\\\",\\\"trace_id\\\":\\\"4ff43bd2-d427-4049-964c-57fbcad91a00\\\",\\\"correlation_id\\\":\\\"6e92626f-a36d-4c41-ae7c-3b7296df0110\\\",\\\"error_uri\\\":\\\"https://login.microsoftonline.com/error?code=7000215\\\"}\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:39] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:39] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1296,\"provider\":\"office\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:39] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":611076,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"ae1d456f-4b46-4c79-bb7f-bc2e95d48c9f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":611076} {\"correlation_id\":\"ae1d456f-4b46-4c79-bb7f-bc2e95d48c9f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":611076,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"ae1d456f-4b46-4c79-bb7f-bc2e95d48c9f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":611076,\"participants\":[{\"id\":997346,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":997347,\"user_id\":18,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"ae1d456f-4b46-4c79-bb7f-bc2e95d48c9f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":391,\"provider\":\"office\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:39] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":391,\"provider\":\"office\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:39] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:39] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":391,\"provider\":\"office\",\"refreshToken\":\"00045eebae0f39b34887c6d53f92ae78064f7145e1f4b67754aebd03cfb2d881\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:39] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"ae1d456f-4b46-4c79-bb7f-bc2e95d48c9f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"ae1d456f-4b46-4c79-bb7f-bc2e95d48c9f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"ae1d456f-4b46-4c79-bb7f-bc2e95d48c9f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"ae1d456f-4b46-4c79-bb7f-bc2e95d48c9f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"ae1d456f-4b46-4c79-bb7f-bc2e95d48c9f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"ae1d456f-4b46-4c79-bb7f-bc2e95d48c9f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"ae1d456f-4b46-4c79-bb7f-bc2e95d48c9f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":611076,\"team_id\":2,\"email\":\"veselin.kulov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"ae1d456f-4b46-4c79-bb7f-bc2e95d48c9f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":611076,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"ae1d456f-4b46-4c79-bb7f-bc2e95d48c9f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":611076} {\"correlation_id\":\"ae1d456f-4b46-4c79-bb7f-bc2e95d48c9f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":611076,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"ae1d456f-4b46-4c79-bb7f-bc2e95d48c9f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":611076,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"ae1d456f-4b46-4c79-bb7f-bc2e95d48c9f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1502,\"provider\":\"google\"} {\"correlation_id\":\"0fdd7dce-04e2-4696-ba87-8089a739c81c\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1502,\"provider\":\"google\"} {\"correlation_id\":\"0fdd7dce-04e2-4696-ba87-8089a739c81c\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:40] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"0fdd7dce-04e2-4696-ba87-8089a739c81c\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:40] local.INFO: [Calendar] Processing sync {\"calendarId\":\"a33076c1-8d97-431a-99f0-85c9524e118b\",\"from\":null,\"to\":null,\"delta\":\"CIiFh8TP44kDEIiFh8TP44kDGAUgkZvkzgIokZvkzgI=\",\"last_sync\":\"2024-12-09 07:12:53\",\"dateMode\":\"daily\"} {\"correlation_id\":\"0fdd7dce-04e2-4696-ba87-8089a739c81c\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:40] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"integration-app\",\"crm_owner\":1695,\"team_id\":3143} {\"correlation_id\":\"0fdd7dce-04e2-4696-ba87-8089a739c81c\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1502,\"provider\":\"google\"} {\"correlation_id\":\"0fdd7dce-04e2-4696-ba87-8089a739c81c\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1502,\"provider\":\"google\"} {\"correlation_id\":\"0fdd7dce-04e2-4696-ba87-8089a739c81c\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:40] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"0fdd7dce-04e2-4696-ba87-8089a739c81c\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":610935,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"9f5579a5-e89f-4074-9843-d5c1f123878d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610935} {\"correlation_id\":\"9f5579a5-e89f-4074-9843-d5c1f123878d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610935,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"9f5579a5-e89f-4074-9843-d5c1f123878d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":610935,\"participants\":[{\"id\":997141,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":997142,\"user_id\":18,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"9f5579a5-e89f-4074-9843-d5c1f123878d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"9f5579a5-e89f-4074-9843-d5c1f123878d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"9f5579a5-e89f-4074-9843-d5c1f123878d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"9f5579a5-e89f-4074-9843-d5c1f123878d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"9f5579a5-e89f-4074-9843-d5c1f123878d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"9f5579a5-e89f-4074-9843-d5c1f123878d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"9f5579a5-e89f-4074-9843-d5c1f123878d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"9f5579a5-e89f-4074-9843-d5c1f123878d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":610935,\"team_id\":2,\"email\":\"veselin.kulov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"9f5579a5-e89f-4074-9843-d5c1f123878d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":610935,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"9f5579a5-e89f-4074-9843-d5c1f123878d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610935} {\"correlation_id\":\"9f5579a5-e89f-4074-9843-d5c1f123878d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610935,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"9f5579a5-e89f-4074-9843-d5c1f123878d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":610935,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"9f5579a5-e89f-4074-9843-d5c1f123878d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":610915,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"ee1e1cdc-f0b6-4353-b761-ada15282a954\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610915} {\"correlation_id\":\"ee1e1cdc-f0b6-4353-b761-ada15282a954\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610915,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"ee1e1cdc-f0b6-4353-b761-ada15282a954\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":610915,\"participants\":[{\"id\":997104,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":997105,\"user_id\":18,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"ee1e1cdc-f0b6-4353-b761-ada15282a954\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"ee1e1cdc-f0b6-4353-b761-ada15282a954\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"ee1e1cdc-f0b6-4353-b761-ada15282a954\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"ee1e1cdc-f0b6-4353-b761-ada15282a954\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"ee1e1cdc-f0b6-4353-b761-ada15282a954\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"ee1e1cdc-f0b6-4353-b761-ada15282a954\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"ee1e1cdc-f0b6-4353-b761-ada15282a954\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"ee1e1cdc-f0b6-4353-b761-ada15282a954\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":610915,\"team_id\":2,\"email\":\"veselin.kulov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"ee1e1cdc-f0b6-4353-b761-ada15282a954\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":610915,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"ee1e1cdc-f0b6-4353-b761-ada15282a954\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610915} {\"correlation_id\":\"ee1e1cdc-f0b6-4353-b761-ada15282a954\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610915,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"ee1e1cdc-f0b6-4353-b761-ada15282a954\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":610915,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"ee1e1cdc-f0b6-4353-b761-ada15282a954\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":610900,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"46958b32-a07f-4aa9-a052-c6a1e3bf70b2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610900} {\"correlation_id\":\"46958b32-a07f-4aa9-a052-c6a1e3bf70b2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610900,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"46958b32-a07f-4aa9-a052-c6a1e3bf70b2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":610900,\"participants\":[{\"id\":997081,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":997082,\"user_id\":18,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"46958b32-a07f-4aa9-a052-c6a1e3bf70b2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"46958b32-a07f-4aa9-a052-c6a1e3bf70b2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"46958b32-a07f-4aa9-a052-c6a1e3bf70b2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"46958b32-a07f-4aa9-a052-c6a1e3bf70b2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"46958b32-a07f-4aa9-a052-c6a1e3bf70b2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"46958b32-a07f-4aa9-a052-c6a1e3bf70b2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"46958b32-a07f-4aa9-a052-c6a1e3bf70b2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"46958b32-a07f-4aa9-a052-c6a1e3bf70b2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":610900,\"team_id\":2,\"email\":\"veselin.kulov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"46958b32-a07f-4aa9-a052-c6a1e3bf70b2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":610900,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"46958b32-a07f-4aa9-a052-c6a1e3bf70b2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610900} {\"correlation_id\":\"46958b32-a07f-4aa9-a052-c6a1e3bf70b2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610900,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"46958b32-a07f-4aa9-a052-c6a1e3bf70b2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":610900,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"46958b32-a07f-4aa9-a052-c6a1e3bf70b2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":610885,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"b9dc6bd0-a276-4d4c-ad70-8cd444119160\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610885} {\"correlation_id\":\"b9dc6bd0-a276-4d4c-ad70-8cd444119160\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610885,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"b9dc6bd0-a276-4d4c-ad70-8cd444119160\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":610885,\"participants\":[{\"id\":997051,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":997052,\"user_id\":18,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"b9dc6bd0-a276-4d4c-ad70-8cd444119160\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"b9dc6bd0-a276-4d4c-ad70-8cd444119160\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"b9dc6bd0-a276-4d4c-ad70-8cd444119160\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"b9dc6bd0-a276-4d4c-ad70-8cd444119160\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"b9dc6bd0-a276-4d4c-ad70-8cd444119160\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"b9dc6bd0-a276-4d4c-ad70-8cd444119160\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"b9dc6bd0-a276-4d4c-ad70-8cd444119160\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"b9dc6bd0-a276-4d4c-ad70-8cd444119160\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":610885,\"team_id\":2,\"email\":\"veselin.kulov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"b9dc6bd0-a276-4d4c-ad70-8cd444119160\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":610885,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"b9dc6bd0-a276-4d4c-ad70-8cd444119160\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610885} {\"correlation_id\":\"b9dc6bd0-a276-4d4c-ad70-8cd444119160\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610885,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"b9dc6bd0-a276-4d4c-ad70-8cd444119160\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":610885,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"b9dc6bd0-a276-4d4c-ad70-8cd444119160\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":610878,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"117202f1-4432-416d-922c-d7cd0f2ae201\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610878} {\"correlation_id\":\"117202f1-4432-416d-922c-d7cd0f2ae201\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610878,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"117202f1-4432-416d-922c-d7cd0f2ae201\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":610878,\"participants\":[{\"id\":997035,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":997036,\"user_id\":18,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"117202f1-4432-416d-922c-d7cd0f2ae201\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":391,\"provider\":\"office\",\"responseBody\":\"{\\\"error\\\":\\\"invalid_client\\\",\\\"error_description\\\":\\\"AADSTS7000215: Invalid client secret provided. Ensure the secret being sent in the request is the client secret value, not the client secret ID, for a secret added to app 'bbcbb2ef-6200-4fae-82bd-d81f5dd738da'. Trace ID: 72b211a8-6a7c-4954-b720-f1a47e981b00 Correlation ID: 5f9781fe-b28a-44ce-94e3-42c683cdfd60 Timestamp: 2026-05-11 10:43:40Z\\\",\\\"error_codes\\\":[7000215],\\\"timestamp\\\":\\\"2026-05-11 10:43:40Z\\\",\\\"trace_id\\\":\\\"72b211a8-6a7c-4954-b720-f1a47e981b00\\\",\\\"correlation_id\\\":\\\"5f9781fe-b28a-44ce-94e3-42c683cdfd60\\\",\\\"error_uri\\\":\\\"https://login.microsoftonline.com/error?code=7000215\\\"}\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:40] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":391,\"provider\":\"office\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"117202f1-4432-416d-922c-d7cd0f2ae201\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"117202f1-4432-416d-922c-d7cd0f2ae201\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"117202f1-4432-416d-922c-d7cd0f2ae201\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"117202f1-4432-416d-922c-d7cd0f2ae201\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1271,\"provider\":\"office\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1271,\"provider\":\"office\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:40] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"117202f1-4432-416d-922c-d7cd0f2ae201\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1271,\"provider\":\"office\",\"refreshToken\":\"118cde2c06993147b07ccaec4cbcd5026a819dea6c71081166a492933e392afb\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"117202f1-4432-416d-922c-d7cd0f2ae201\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"117202f1-4432-416d-922c-d7cd0f2ae201\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":610878,\"team_id\":2,\"email\":\"veselin.kulov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"117202f1-4432-416d-922c-d7cd0f2ae201\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":610878,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"117202f1-4432-416d-922c-d7cd0f2ae201\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610878} {\"correlation_id\":\"117202f1-4432-416d-922c-d7cd0f2ae201\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610878,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"117202f1-4432-416d-922c-d7cd0f2ae201\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":610878,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"117202f1-4432-416d-922c-d7cd0f2ae201\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Google Calendar] Failed to watch channel for calendar {\"calendarId\":\"a33076c1-8d97-431a-99f0-85c9524e118b\",\"code\":400,\"reason\":\"{\n \\\"error\\\": {\n \\\"errors\\\": [\n {\n \\\"domain\\\": \\\"global\\\",\n \\\"reason\\\": \\\"push.webhookUrlNotHttps\\\",\n \\\"message\\\": \\\"WebHook callback must be HTTPS: /webhook/calendar/google?resourceType=event\\\"\n }\n ],\n \\\"code\\\": 400,\n \\\"message\\\": \\\"WebHook callback must be HTTPS: /webhook/calendar/google?resourceType=event\\\"\n }\n}\"} {\"correlation_id\":\"0fdd7dce-04e2-4696-ba87-8089a739c81c\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:40] local.WARNING: [Calendar] Sync failed {\"calendarId\":\"a33076c1-8d97-431a-99f0-85c9524e118b\",\"code\":400,\"reason\":\"{\n \\\"error\\\": {\n \\\"errors\\\": [\n {\n \\\"domain\\\": \\\"global\\\",\n \\\"reason\\\": \\\"push.webhookUrlNotHttps\\\",\n \\\"message\\\": \\\"WebHook callback must be HTTPS: /webhook/calendar/google?resourceType=event\\\"\n }\n ],\n \\\"code\\\": 400,\n \\\"message\\\": \\\"WebHook callback must be HTTPS: /webhook/calendar/google?resourceType=event\\\"\n }\n}\"} {\"correlation_id\":\"0fdd7dce-04e2-4696-ba87-8089a739c81c\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":610874,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"6264f69a-8461-4d85-9725-ef4c30fb5d35\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610874} {\"correlation_id\":\"6264f69a-8461-4d85-9725-ef4c30fb5d35\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610874,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"6264f69a-8461-4d85-9725-ef4c30fb5d35\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":610874,\"participants\":[{\"id\":997025,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":997026,\"user_id\":18,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"6264f69a-8461-4d85-9725-ef4c30fb5d35\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"6264f69a-8461-4d85-9725-ef4c30fb5d35\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"6264f69a-8461-4d85-9725-ef4c30fb5d35\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"6264f69a-8461-4d85-9725-ef4c30fb5d35\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"6264f69a-8461-4d85-9725-ef4c30fb5d35\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"6264f69a-8461-4d85-9725-ef4c30fb5d35\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"6264f69a-8461-4d85-9725-ef4c30fb5d35\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"6264f69a-8461-4d85-9725-ef4c30fb5d35\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":610874,\"team_id\":2,\"email\":\"veselin.kulov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"6264f69a-8461-4d85-9725-ef4c30fb5d35\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":610874,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"6264f69a-8461-4d85-9725-ef4c30fb5d35\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610874} {\"correlation_id\":\"6264f69a-8461-4d85-9725-ef4c30fb5d35\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610874,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"6264f69a-8461-4d85-9725-ef4c30fb5d35\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":610874,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"6264f69a-8461-4d85-9725-ef4c30fb5d35\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":610867,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"978698d8-e33a-4d4a-b374-04919cae6ac2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610867} {\"correlation_id\":\"978698d8-e33a-4d4a-b374-04919cae6ac2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610867,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"978698d8-e33a-4d4a-b374-04919cae6ac2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":610867,\"participants\":[{\"id\":997011,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":997012,\"user_id\":18,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"978698d8-e33a-4d4a-b374-04919cae6ac2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"978698d8-e33a-4d4a-b374-04919cae6ac2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"978698d8-e33a-4d4a-b374-04919cae6ac2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"978698d8-e33a-4d4a-b374-04919cae6ac2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"978698d8-e33a-4d4a-b374-04919cae6ac2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"978698d8-e33a-4d4a-b374-04919cae6ac2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"978698d8-e33a-4d4a-b374-04919cae6ac2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"978698d8-e33a-4d4a-b374-04919cae6ac2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":610867,\"team_id\":2,\"email\":\"veselin.kulov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"978698d8-e33a-4d4a-b374-04919cae6ac2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":610867,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"978698d8-e33a-4d4a-b374-04919cae6ac2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610867} {\"correlation_id\":\"978698d8-e33a-4d4a-b374-04919cae6ac2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610867,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"978698d8-e33a-4d4a-b374-04919cae6ac2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":610867,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"978698d8-e33a-4d4a-b374-04919cae6ac2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":610764,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"3e294e41-3a19-4afe-87b3-4f7f0336107c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610764} {\"correlation_id\":\"3e294e41-3a19-4afe-87b3-4f7f0336107c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610764,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"3e294e41-3a19-4afe-87b3-4f7f0336107c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":610764,\"participants\":[{\"id\":996951,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":996952,\"user_id\":18,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"3e294e41-3a19-4afe-87b3-4f7f0336107c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"3e294e41-3a19-4afe-87b3-4f7f0336107c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"3e294e41-3a19-4afe-87b3-4f7f0336107c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"3e294e41-3a19-4afe-87b3-4f7f0336107c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"3e294e41-3a19-4afe-87b3-4f7f0336107c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"3e294e41-3a19-4afe-87b3-4f7f0336107c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"3e294e41-3a19-4afe-87b3-4f7f0336107c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"3e294e41-3a19-4afe-87b3-4f7f0336107c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":610764,\"team_id\":2,\"email\":\"veselin.kulov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"3e294e41-3a19-4afe-87b3-4f7f0336107c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":610764,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"3e294e41-3a19-4afe-87b3-4f7f0336107c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610764} {\"correlation_id\":\"3e294e41-3a19-4afe-87b3-4f7f0336107c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610764,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"3e294e41-3a19-4afe-87b3-4f7f0336107c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":610764,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"3e294e41-3a19-4afe-87b3-4f7f0336107c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":610617,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"732b2bbb-ee7f-4bb2-85b1-e6478955544b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610617} {\"correlation_id\":\"732b2bbb-ee7f-4bb2-85b1-e6478955544b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610617,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"732b2bbb-ee7f-4bb2-85b1-e6478955544b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":610617,\"participants\":[{\"id\":996641,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":996642,\"user_id\":18,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"732b2bbb-ee7f-4bb2-85b1-e6478955544b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"732b2bbb-ee7f-4bb2-85b1-e6478955544b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"732b2bbb-ee7f-4bb2-85b1-e6478955544b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"732b2bbb-ee7f-4bb2-85b1-e6478955544b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"732b2bbb-ee7f-4bb2-85b1-e6478955544b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"732b2bbb-ee7f-4bb2-85b1-e6478955544b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"732b2bbb-ee7f-4bb2-85b1-e6478955544b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"732b2bbb-ee7f-4bb2-85b1-e6478955544b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":610617,\"team_id\":2,\"email\":\"veselin.kulov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"732b2bbb-ee7f-4bb2-85b1-e6478955544b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":610617,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"732b2bbb-ee7f-4bb2-85b1-e6478955544b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610617} {\"correlation_id\":\"732b2bbb-ee7f-4bb2-85b1-e6478955544b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610617,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"732b2bbb-ee7f-4bb2-85b1-e6478955544b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":610617,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"732b2bbb-ee7f-4bb2-85b1-e6478955544b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":610539,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"d94f0ac2-c007-4b7d-9fd3-27fc32f9b9a2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610539} {\"correlation_id\":\"d94f0ac2-c007-4b7d-9fd3-27fc32f9b9a2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610539,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"d94f0ac2-c007-4b7d-9fd3-27fc32f9b9a2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":610539,\"participants\":[{\"id\":996485,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":996486,\"user_id\":18,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"d94f0ac2-c007-4b7d-9fd3-27fc32f9b9a2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"d94f0ac2-c007-4b7d-9fd3-27fc32f9b9a2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"d94f0ac2-c007-4b7d-9fd3-27fc32f9b9a2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d94f0ac2-c007-4b7d-9fd3-27fc32f9b9a2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"d94f0ac2-c007-4b7d-9fd3-27fc32f9b9a2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"d94f0ac2-c007-4b7d-9fd3-27fc32f9b9a2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"d94f0ac2-c007-4b7d-9fd3-27fc32f9b9a2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"d94f0ac2-c007-4b7d-9fd3-27fc32f9b9a2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":610539,\"team_id\":2,\"email\":\"veselin.kulov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"d94f0ac2-c007-4b7d-9fd3-27fc32f9b9a2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":610539,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"d94f0ac2-c007-4b7d-9fd3-27fc32f9b9a2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610539} {\"correlation_id\":\"d94f0ac2-c007-4b7d-9fd3-27fc32f9b9a2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610539,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"d94f0ac2-c007-4b7d-9fd3-27fc32f9b9a2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":610539,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"d94f0ac2-c007-4b7d-9fd3-27fc32f9b9a2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":610528,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"c316e103-4e48-4f28-8bc6-206714fc00ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610528} {\"correlation_id\":\"c316e103-4e48-4f28-8bc6-206714fc00ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610528,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"c316e103-4e48-4f28-8bc6-206714fc00ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":610528,\"participants\":[{\"id\":996463,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":996464,\"user_id\":18,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"c316e103-4e48-4f28-8bc6-206714fc00ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"c316e103-4e48-4f28-8bc6-206714fc00ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"c316e103-4e48-4f28-8bc6-206714fc00ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"c316e103-4e48-4f28-8bc6-206714fc00ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"c316e103-4e48-4f28-8bc6-206714fc00ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"c316e103-4e48-4f28-8bc6-206714fc00ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"c316e103-4e48-4f28-8bc6-206714fc00ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"c316e103-4e48-4f28-8bc6-206714fc00ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":610528,\"team_id\":2,\"email\":\"veselin.kulov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"c316e103-4e48-4f28-8bc6-206714fc00ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":610528,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"c316e103-4e48-4f28-8bc6-206714fc00ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610528} {\"correlation_id\":\"c316e103-4e48-4f28-8bc6-206714fc00ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610528,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"c316e103-4e48-4f28-8bc6-206714fc00ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":610528,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"c316e103-4e48-4f28-8bc6-206714fc00ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":610506,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"f83f62c6-544e-4551-8bc2-99f405d022e6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610506} {\"correlation_id\":\"f83f62c6-544e-4551-8bc2-99f405d022e6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610506,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"f83f62c6-544e-4551-8bc2-99f405d022e6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":610506,\"participants\":[{\"id\":996419,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":996420,\"user_id\":18,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"f83f62c6-544e-4551-8bc2-99f405d022e6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"f83f62c6-544e-4551-8bc2-99f405d022e6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"f83f62c6-544e-4551-8bc2-99f405d022e6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"f83f62c6-544e-4551-8bc2-99f405d022e6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"f83f62c6-544e-4551-8bc2-99f405d022e6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"f83f62c6-544e-4551-8bc2-99f405d022e6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"f83f62c6-544e-4551-8bc2-99f405d022e6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"f83f62c6-544e-4551-8bc2-99f405d022e6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1271,\"provider\":\"office\",\"responseBody\":\"{\\\"error\\\":\\\"invalid_client\\\",\\\"error_description\\\":\\\"AADSTS7000215: Invalid client secret provided. Ensure the secret being sent in the request is the client secret value, not the client secret ID, for a secret added to app 'bbcbb2ef-6200-4fae-82bd-d81f5dd738da'. Trace ID: 8addf5ba-9879-4822-b2bb-e5002b0d2200 Correlation ID: feece826-abbe-4c27-85d0-0cddd6b33c3b Timestamp: 2026-05-11 10:43:41Z\\\",\\\"error_codes\\\":[7000215],\\\"timestamp\\\":\\\"2026-05-11 10:43:41Z\\\",\\\"trace_id\\\":\\\"8addf5ba-9879-4822-b2bb-e5002b0d2200\\\",\\\"correlation_id\\\":\\\"feece826-abbe-4c27-85d0-0cddd6b33c3b\\\",\\\"error_uri\\\":\\\"https://login.microsoftonline.com/error?code=7000215\\\"}\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1271,\"provider\":\"office\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":610506,\"team_id\":2,\"email\":\"veselin.kulov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"f83f62c6-544e-4551-8bc2-99f405d022e6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":610506,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"f83f62c6-544e-4551-8bc2-99f405d022e6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610506} {\"correlation_id\":\"f83f62c6-544e-4551-8bc2-99f405d022e6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610506,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"f83f62c6-544e-4551-8bc2-99f405d022e6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1351,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":610506,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"f83f62c6-544e-4551-8bc2-99f405d022e6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1351,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1351,\"provider\":\"google\",\"refreshToken\":\"4271d15b9e60a606439caddc68337f783e472c85b03dacff14d1b6dfded9051c\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":610497,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"854a8b20-3fc0-4ea6-a7c0-e0ddd599dd3b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610497} {\"correlation_id\":\"854a8b20-3fc0-4ea6-a7c0-e0ddd599dd3b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610497,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"854a8b20-3fc0-4ea6-a7c0-e0ddd599dd3b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":610497,\"participants\":[{\"id\":996401,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":996402,\"user_id\":18,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"854a8b20-3fc0-4ea6-a7c0-e0ddd599dd3b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1351,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Bad Request\"}} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1351,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"854a8b20-3fc0-4ea6-a7c0-e0ddd599dd3b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"854a8b20-3fc0-4ea6-a7c0-e0ddd599dd3b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"854a8b20-3fc0-4ea6-a7c0-e0ddd599dd3b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"854a8b20-3fc0-4ea6-a7c0-e0ddd599dd3b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1366,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1366,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"854a8b20-3fc0-4ea6-a7c0-e0ddd599dd3b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"854a8b20-3fc0-4ea6-a7c0-e0ddd599dd3b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1366,\"provider\":\"google\",\"refreshToken\":\"ae21385059b2eebfd43f68aecd56eccd702a1aabb6598f1f7ab594ed8af491b4\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"854a8b20-3fc0-4ea6-a7c0-e0ddd599dd3b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":610497,\"team_id\":2,\"email\":\"veselin.kulov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"854a8b20-3fc0-4ea6-a7c0-e0ddd599dd3b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":610497,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"854a8b20-3fc0-4ea6-a7c0-e0ddd599dd3b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610497} {\"correlation_id\":\"854a8b20-3fc0-4ea6-a7c0-e0ddd599dd3b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610497,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"854a8b20-3fc0-4ea6-a7c0-e0ddd599dd3b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":610497,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"854a8b20-3fc0-4ea6-a7c0-e0ddd599dd3b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":610490,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"bab4020c-8e0d-42eb-afdb-e6bab67d42c7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610490} {\"correlation_id\":\"bab4020c-8e0d-42eb-afdb-e6bab67d42c7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610490,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"bab4020c-8e0d-42eb-afdb-e6bab67d42c7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":610490,\"participants\":[{\"id\":996385,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":996386,\"user_id\":18,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"bab4020c-8e0d-42eb-afdb-e6bab67d42c7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"bab4020c-8e0d-42eb-afdb-e6bab67d42c7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"bab4020c-8e0d-42eb-afdb-e6bab67d42c7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"bab4020c-8e0d-42eb-afdb-e6bab67d42c7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"bab4020c-8e0d-42eb-afdb-e6bab67d42c7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"bab4020c-8e0d-42eb-afdb-e6bab67d42c7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"bab4020c-8e0d-42eb-afdb-e6bab67d42c7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"bab4020c-8e0d-42eb-afdb-e6bab67d42c7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":610490,\"team_id\":2,\"email\":\"veselin.kulov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"bab4020c-8e0d-42eb-afdb-e6bab67d42c7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":610490,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"bab4020c-8e0d-42eb-afdb-e6bab67d42c7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610490} {\"correlation_id\":\"bab4020c-8e0d-42eb-afdb-e6bab67d42c7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610490,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"bab4020c-8e0d-42eb-afdb-e6bab67d42c7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":610490,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"bab4020c-8e0d-42eb-afdb-e6bab67d42c7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1366,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Bad Request\"}} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1366,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":610470,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"55223d65-4d62-42ac-994b-ae4c8f27cbef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1115,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610470} {\"correlation_id\":\"55223d65-4d62-42ac-994b-ae4c8f27cbef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1115,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610470,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"55223d65-4d62-42ac-994b-ae4c8f27cbef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":610470,\"participants\":[{\"id\":996369,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":996370,\"user_id\":18,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"55223d65-4d62-42ac-994b-ae4c8f27cbef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: Calendar sync job dispatched {\"calendar_id\":378} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1421,\"provider\":\"office\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1421,\"provider\":\"office\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"55223d65-4d62-42ac-994b-ae4c8f27cbef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"55223d65-4d62-42ac-994b-ae4c8f27cbef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"55223d65-4d62-42ac-994b-ae4c8f27cbef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: Calendar sync job dispatched {\"calendar_id\":504} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"55223d65-4d62-42ac-994b-ae4c8f27cbef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"55223d65-4d62-42ac-994b-ae4c8f27cbef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"55223d65-4d62-42ac-994b-ae4c8f27cbef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"55223d65-4d62-42ac-994b-ae4c8f27cbef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.NOTICE: Calendar sync end {\"retrieved_calendars\":31,\"processed_calendars\":3} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"calendar:sync\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":610470,\"team_id\":2,\"email\":\"veselin.kulov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"55223d65-4d62-42ac-994b-ae4c8f27cbef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":610470,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"55223d65-4d62-42ac-994b-ae4c8f27cbef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610470} {\"correlation_id\":\"55223d65-4d62-42ac-994b-ae4c8f27cbef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610470,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"55223d65-4d62-42ac-994b-ae4c8f27cbef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":610470,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"55223d65-4d62-42ac-994b-ae4c8f27cbef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":610462,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"0108a9c2-fb98-43fc-ad71-084c53b40936\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610462} {\"correlation_id\":\"0108a9c2-fb98-43fc-ad71-084c53b40936\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610462,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"0108a9c2-fb98-43fc-ad71-084c53b40936\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":610462,\"participants\":[{\"id\":996353,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":996354,\"user_id\":18,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"0108a9c2-fb98-43fc-ad71-084c53b40936\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"0108a9c2-fb98-43fc-ad71-084c53b40936\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"0108a9c2-fb98-43fc-ad71-084c53b40936\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"0108a9c2-fb98-43fc-ad71-084c53b40936\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"0108a9c2-fb98-43fc-ad71-084c53b40936\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"0108a9c2-fb98-43fc-ad71-084c53b40936\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"0108a9c2-fb98-43fc-ad71-084c53b40936\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"0108a9c2-fb98-43fc-ad71-084c53b40936\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":610462,\"team_id\":2,\"email\":\"veselin.kulov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"0108a9c2-fb98-43fc-ad71-084c53b40936\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":610462,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"0108a9c2-fb98-43fc-ad71-084c53b40936\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610462} {\"correlation_id\":\"0108a9c2-fb98-43fc-ad71-084c53b40936\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610462,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"0108a9c2-fb98-43fc-ad71-084c53b40936\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":610462,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"0108a9c2-fb98-43fc-ad71-084c53b40936\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":610451,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"d1fa1d3a-52aa-496e-8905-56f7157299b1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610451} {\"correlation_id\":\"d1fa1d3a-52aa-496e-8905-56f7157299b1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610451,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"d1fa1d3a-52aa-496e-8905-56f7157299b1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":610451,\"participants\":[{\"id\":996340,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":996341,\"user_id\":18,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"d1fa1d3a-52aa-496e-8905-56f7157299b1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"d1fa1d3a-52aa-496e-8905-56f7157299b1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"d1fa1d3a-52aa-496e-8905-56f7157299b1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d1fa1d3a-52aa-496e-8905-56f7157299b1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"d1fa1d3a-52aa-496e-8905-56f7157299b1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"d1fa1d3a-52aa-496e-8905-56f7157299b1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"d1fa1d3a-52aa-496e-8905-56f7157299b1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"d1fa1d3a-52aa-496e-8905-56f7157299b1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":610451,\"team_id\":2,\"email\":\"veselin.kulov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"d1fa1d3a-52aa-496e-8905-56f7157299b1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":610451,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"d1fa1d3a-52aa-496e-8905-56f7157299b1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610451} {\"correlation_id\":\"d1fa1d3a-52aa-496e-8905-56f7157299b1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610451,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"d1fa1d3a-52aa-496e-8905-56f7157299b1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":610451,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"d1fa1d3a-52aa-496e-8905-56f7157299b1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":610438,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"da3f7962-a65d-4bfe-80bf-c33b4ccd36f5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610438} {\"correlation_id\":\"da3f7962-a65d-4bfe-80bf-c33b4ccd36f5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610438,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"da3f7962-a65d-4bfe-80bf-c33b4ccd36f5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":610438,\"participants\":[{\"id\":996320,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":996321,\"user_id\":18,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"da3f7962-a65d-4bfe-80bf-c33b4ccd36f5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"da3f7962-a65d-4bfe-80bf-c33b4ccd36f5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"da3f7962-a65d-4bfe-80bf-c33b4ccd36f5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"da3f7962-a65d-4bfe-80bf-c33b4ccd36f5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"da3f7962-a65d-4bfe-80bf-c33b4ccd36f5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"da3f7962-a65d-4bfe-80bf-c33b4ccd36f5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"da3f7962-a65d-4bfe-80bf-c33b4ccd36f5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"da3f7962-a65d-4bfe-80bf-c33b4ccd36f5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":610438,\"team_id\":2,\"email\":\"veselin.kulov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"da3f7962-a65d-4bfe-80bf-c33b4ccd36f5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":610438,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"da3f7962-a65d-4bfe-80bf-c33b4ccd36f5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610438} {\"correlation_id\":\"da3f7962-a65d-4bfe-80bf-c33b4ccd36f5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610438,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"da3f7962-a65d-4bfe-80bf-c33b4ccd36f5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":610438,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"da3f7962-a65d-4bfe-80bf-c33b4ccd36f5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":610426,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"da7b40c8-e233-4cf9-a133-31f12397ad10\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610426} {\"correlation_id\":\"da7b40c8-e233-4cf9-a133-31f12397ad10\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610426,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"da7b40c8-e233-4cf9-a133-31f12397ad10\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":610426,\"participants\":[{\"id\":996306,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":996307,\"user_id\":18,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"da7b40c8-e233-4cf9-a133-31f12397ad10\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"da7b40c8-e233-4cf9-a133-31f12397ad10\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"da7b40c8-e233-4cf9-a133-31f12397ad10\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"da7b40c8-e233-4cf9-a133-31f12397ad10\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"da7b40c8-e233-4cf9-a133-31f12397ad10\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"da7b40c8-e233-4cf9-a133-31f12397ad10\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"da7b40c8-e233-4cf9-a133-31f12397ad10\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"da7b40c8-e233-4cf9-a133-31f12397ad10\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":610426,\"team_id\":2,\"email\":\"veselin.kulov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"da7b40c8-e233-4cf9-a133-31f12397ad10\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":610426,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"da7b40c8-e233-4cf9-a133-31f12397ad10\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610426} {\"correlation_id\":\"da7b40c8-e233-4cf9-a133-31f12397ad10\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610426,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"da7b40c8-e233-4cf9-a133-31f12397ad10\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":610426,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"da7b40c8-e233-4cf9-a133-31f12397ad10\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":610403,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"4e1aa6ad-3e68-46fe-8129-19feb67cee5f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610403} {\"correlation_id\":\"4e1aa6ad-3e68-46fe-8129-19feb67cee5f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610403,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"4e1aa6ad-3e68-46fe-8129-19feb67cee5f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":610403,\"participants\":[{\"id\":996282,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":996283,\"user_id\":18,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"4e1aa6ad-3e68-46fe-8129-19feb67cee5f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"4e1aa6ad-3e68-46fe-8129-19feb67cee5f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"4e1aa6ad-3e68-46fe-8129-19feb67cee5f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4e1aa6ad-3e68-46fe-8129-19feb67cee5f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"4e1aa6ad-3e68-46fe-8129-19feb67cee5f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"4e1aa6ad-3e68-46fe-8129-19feb67cee5f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"4e1aa6ad-3e68-46fe-8129-19feb67cee5f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"4e1aa6ad-3e68-46fe-8129-19feb67cee5f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":610403,\"team_id\":2,\"email\":\"veselin.kulov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"4e1aa6ad-3e68-46fe-8129-19feb67cee5f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":610403,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"4e1aa6ad-3e68-46fe-8129-19feb67cee5f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610403} {\"correlation_id\":\"4e1aa6ad-3e68-46fe-8129-19feb67cee5f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610403,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"4e1aa6ad-3e68-46fe-8129-19feb67cee5f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":610403,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"4e1aa6ad-3e68-46fe-8129-19feb67cee5f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":610400,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":350,\"stage_id\":34}} {\"correlation_id\":\"c137d466-9e9c-4af1-82c0-9f8639805989\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610400} {\"correlation_id\":\"c137d466-9e9c-4af1-82c0-9f8639805989\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610400,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"c137d466-9e9c-4af1-82c0-9f8639805989\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":610400,\"participants\":[{\"id\":996275,\"user_id\":1460,\"contact_id\":null,\"lead_id\":null},{\"id\":996276,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":996277,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null}]} {\"correlation_id\":\"c137d466-9e9c-4af1-82c0-9f8639805989\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"c137d466-9e9c-4af1-82c0-9f8639805989\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"c137d466-9e9c-4af1-82c0-9f8639805989\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"c137d466-9e9c-4af1-82c0-9f8639805989\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"c137d466-9e9c-4af1-82c0-9f8639805989\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":610400,\"team_id\":2,\"email\":\"aneliya.angelova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"c137d466-9e9c-4af1-82c0-9f8639805989\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":610400,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"c137d466-9e9c-4af1-82c0-9f8639805989\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: ProspectCache - Searching DB for opportunity by owner {\"account_id\":244,\"contact_id\":4487,\"owner_id\":1460} {\"correlation_id\":\"c137d466-9e9c-4af1-82c0-9f8639805989\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: ProspectCache - Fallback DB opportunity search {\"account_id\":244,\"contact_id\":4487} {\"correlation_id\":\"c137d466-9e9c-4af1-82c0-9f8639805989\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: ProspectCache - Opportunity DB search results {\"account_id\":244,\"contact_id\":4487,\"opportunity_id\":350} {\"correlation_id\":\"c137d466-9e9c-4af1-82c0-9f8639805989\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"c137d466-9e9c-4af1-82c0-9f8639805989\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":610400,\"participants_processed\":3,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"c137d466-9e9c-4af1-82c0-9f8639805989\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610400} {\"correlation_id\":\"c137d466-9e9c-4af1-82c0-9f8639805989\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610400,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"c137d466-9e9c-4af1-82c0-9f8639805989\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":610400,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":350,\"stage_id\":34} {\"correlation_id\":\"c137d466-9e9c-4af1-82c0-9f8639805989\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1115,\"provider\":\"google\"} {\"correlation_id\":\"003a1645-a7b4-447f-8217-b1f109d902a1\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:42] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1115,\"provider\":\"google\"} {\"correlation_id\":\"003a1645-a7b4-447f-8217-b1f109d902a1\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:42] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"003a1645-a7b4-447f-8217-b1f109d902a1\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:42] local.INFO: [Calendar] Processing sync {\"calendarId\":\"2676cb6d-f86c-427e-bf78-591e388e3c1e\",\"from\":null,\"to\":null,\"delta\":\"CJ_x49O3jpIDEJ_x49O3jpIDGAUgw67KlwMow67KlwM=\",\"last_sync\":\"2026-01-19 07:48:40\",\"dateMode\":\"daily\"} {\"correlation_id\":\"003a1645-a7b4-447f-8217-b1f109d902a1\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:42] local.WARNING: [Pipedrive] Account not connected for user {\"userId\":\"e6538737-e7b4-455f-a37a-3e79b665a220\",\"account\":{\"Jiminny\\\\Models\\\\SocialAccount\":{\"id\":1116,\"sociable_id\":241,\"provider_user_id\":\"19555731\",\"expires\":1775683749,\"refresh_token_expires\":null,\"provider\":\"pipedrive\",\"state\":\"full-refresh\",\"auth_scope\":\"base,deals:full,activities:full,contacts:full,search:read\",\"retry_after\":null,\"created_at\":\"2023-09-08 09:44:29\",\"updated_at\":\"2026-04-08 22:58:34\"}}} {\"correlation_id\":\"003a1645-a7b4-447f-8217-b1f109d902a1\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:42] local.INFO: [CrmOwnerResolver] Integration owner is not connected, attempting team members {\"crm_provider\":\"pipedrive\",\"crm_owner\":241,\"team_id\":19} {\"correlation_id\":\"003a1645-a7b4-447f-8217-b1f109d902a1\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:42] local.INFO: [CrmOwnerResolver] No team members found with active crm connection {\"crm_provider\":\"pipedrive\",\"team_id\":19} {\"correlation_id\":\"003a1645-a7b4-447f-8217-b1f109d902a1\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:42] local.INFO: [CrmOwnerResolver] No team member found with active crm connection {\"crm_provider\":\"pipedrive\",\"team_id\":19} {\"correlation_id\":\"003a1645-a7b4-447f-8217-b1f109d902a1\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:42] local.WARNING: [Calendar] CRM disconnected for user so events will not be matched {\"provider\":\"pipedrive\",\"user_id\":241,\"message\":\"Your Pipedrive account has become disconnected. Please login to Jiminny to reconnect.\"} {\"correlation_id\":\"003a1645-a7b4-447f-8217-b1f109d902a1\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:42] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1115,\"provider\":\"google\"} {\"correlation_id\":\"003a1645-a7b4-447f-8217-b1f109d902a1\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:42] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1115,\"provider\":\"google\"} {\"correlation_id\":\"003a1645-a7b4-447f-8217-b1f109d902a1\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:42] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"003a1645-a7b4-447f-8217-b1f109d902a1\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:42] local.INFO: [Google Calendar] Failed to watch channel for calendar {\"calendarId\":\"2676cb6d-f86c-427e-bf78-591e388e3c1e\",\"code\":400,\"reason\":\"{\n \\\"error\\\": {\n \\\"errors\\\": [\n {\n \\\"domain\\\": \\\"global\\\",\n \\\"reason\\\": \\\"push.webhookUrlNotHttps\\\",\n \\\"message\\\": \\\"WebHook callback must be HTTPS: /webhook/calendar/google?resourceType=event\\\"\n }\n ],\n \\\"code\\\": 400,\n \\\"message\\\": \\\"WebHook callback must be HTTPS: /webhook/calendar/google?resourceType=event\\\"\n }\n}\"} {\"correlation_id\":\"003a1645-a7b4-447f-8217-b1f109d902a1\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:42] local.WARNING: [Calendar] Sync failed {\"calendarId\":\"2676cb6d-f86c-427e-bf78-591e388e3c1e\",\"code\":400,\"reason\":\"{\n \\\"error\\\": {\n \\\"errors\\\": [\n {\n \\\"domain\\\": \\\"global\\\",\n \\\"reason\\\": \\\"push.webhookUrlNotHttps\\\",\n \\\"message\\\": \\\"WebHook callback must be HTTPS: /webhook/calendar/google?resourceType=event\\\"\n }\n ],\n \\\"code\\\": 400,\n \\\"message\\\": \\\"WebHook callback must be HTTPS: /webhook/calendar/google?resourceType=event\\\"\n }\n}\"} {\"correlation_id\":\"003a1645-a7b4-447f-8217-b1f109d902a1\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:42] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1421,\"provider\":\"office\"} {\"correlation_id\":\"f66a7935-f1a6-4d93-b53d-1464e6e79019\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:42] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1421,\"provider\":\"office\"} {\"correlation_id\":\"f66a7935-f1a6-4d93-b53d-1464e6e79019\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:42] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"f66a7935-f1a6-4d93-b53d-1464e6e79019\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:42] local.INFO: [Calendar] Processing sync {\"calendarId\":\"9e8b1a2c-1a8f-42bd-b161-810fc0baf540\",\"from\":null,\"to\":null,\"delta\":\"R0usmcdvmMuZCBYV0hguCLlvcOB4kXlhlC7KgH1SnZwTrZ3faZv1fXPQqJhxe_L9AxWWlb-wASsjGiiWlhsBUg9MFb3ZdlAYerVV_ZirRPbsKWCxEXhybD90arJmok_M4ecGFUQ9_BIGu-c6RAnJy2TRKZ7gPTsJi_8TGceGAuqimlhm4G4mjDLvYVVwImjjU7M3xJvUzL47dLOGNTJCww.k1TST0VEYCgbFOkwa3ysYMi100FtVfkzfqlXLnV6gPg\",\"last_sync\":\"2026-05-11 06:13:36\",\"dateMode\":\"daily\"} {\"correlation_id\":\"f66a7935-f1a6-4d93-b53d-1464e6e79019\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:42] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"f66a7935-f1a6-4d93-b53d-1464e6e79019\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:42] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"f66a7935-f1a6-4d93-b53d-1464e6e79019\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:42] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"f66a7935-f1a6-4d93-b53d-1464e6e79019\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:42] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":89,\"team_id\":2} {\"correlation_id\":\"f66a7935-f1a6-4d93-b53d-1464e6e79019\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:42] local.INFO: [MS Office Calendar] Skipping delta sync for daily mode {\"calendarId\":\"9e8b1a2c-1a8f-42bd-b161-810fc0baf540\"} {\"correlation_id\":\"f66a7935-f1a6-4d93-b53d-1464e6e79019\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}","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}]...
|
879150276765577717
|
5585982875328481636
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
3
70
3
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\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 Illuminate\Support\Facades\Redis;
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)
{
$cacheKey = $this->getRateLimitCacheKey();
$cachedRetryAfter = Redis::get($cacheKey);
if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {
throw new RateLimitException(
'Hubspot rate limit (cached circuit-breaker)',
(int) $cachedRetryAfter,
);
}
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'policy' => $this->parsePolicy($e),
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function getRateLimitCacheKey(): string
{
return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());
}
public function isHubspotRateLimit(Throwable $e): bool
{
if ($e instanceof BadRequest
|| $e instanceof DealApiException
|| $e instanceof ContactApiException
|| $e instanceof CompanyApiException
|| $e instanceof \GuzzleHttp\Exception\RequestException
) {
return (int) $e->getCode() === 429;
}
return false;
}
public function parseRetryAfter(Throwable $e): int
{
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$e ' . PHP_EOL . print_r($e, true));
return 5;
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;
}
}
$policy = $this->parsePolicy($e);
if ($policy === 'TEN_SECONDLY_ROLLING') {
return 10;
}
if ($policy === 'SECONDLY') {
return 1;
}
if ($policy === 'DAILY_LIMIT') {
return 600;
}
$this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [
'exception_class' => get_class($e),
]);
return 10;
}
public function parsePolicy(Throwable $e): ?string
{
if (! method_exists($e, 'getResponseBody')) {
return null;
}
$body = $e->getResponseBody();
if (is_string($body)) {
$body = json_decode($body, true) ?? [];
}
if (! is_array($body)) {
return null;
}
$policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;
return is_string($policy) ? strtoupper($policy) : null;
}
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
);
}
/**
* Execute a search request against HubSpot CRM objects with rate limiting.
*
* @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')
* @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.
* @return array The search response with 'results', 'total', 'paging' keys
* @throws RateLimitException When rate limit is hit
* @throws HubspotException On API errors
*/
public function search(string $objectType, array $payload): array
{
$endpoint = self::BASE_URL . "/crm/v3/objects/{$objectType}/search";
return $this->executeRequest(function () use ($endpoint, $payload) {
$response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$response ' . PHP_EOL . print_r($response, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$headers ' . PHP_EOL . print_r($response->getHeaders(), true));
return $response->toArray();
});
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$deal = $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') {
return $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
return $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
}
/**
* @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 (RateLimitException $e) {
// throw $e;
} 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);
}
}
Show Replace Field
Search History
Received 429 from API
New Line
Match Case
Words
Regex
Replace History
Replace
New Line
Preserve case
0 results
Previous Occurrence
Next Occurrence
Filter Search Results
Open in Window, Multiple Cursors
Click to highlight
Close
Sync Changes
Hide This Notification
Code changed:
Hide
[2026-05-11 10:41:16] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"mailbox:batch:process","memoryBeforeCommandInMb":60.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"ee9b9096-b1f1-4e2b-ac17-af0003eb00c0","trace_id":"8904c61b-d5b4-44cc-91bf-68f0f73222c0"}
[2026-05-11 10:41:16] local.INFO: [EmailSchedule] STARTING batch process {"host":"docker_lamp_1"} {"correlation_id":"ee9b9096-b1f1-4e2b-ac17-af0003eb00c0","trace_id":"8904c61b-d5b4-44cc-91bf-68f0f73222c0"}
[2026-05-11 10:41:17] local.INFO: [EmailSchedule] FINISHED batch process {"host":"docker_lamp_1","processed":0} {"correlation_id":"ee9b9096-b1f1-4e2b-ac17-af0003eb00c0","trace_id":"8904c61b-d5b4-44cc-91bf-68f0f73222c0"}
[2026-05-11 10:41:17] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"mailbox:batch:process","memoryBeforeCommandInMb":60.0,"memoryAfterCommandInMB":60.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"ee9b9096-b1f1-4e2b-ac17-af0003eb00c0","trace_id":"8904c61b-d5b4-44cc-91bf-68f0f73222c0"}
[2026-05-11 10:41:21] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"crm:sync-hubspot-objects","memoryBeforeCommandInMb":60.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"13c8d2de-a598-41db-b4ca-9799a4bee946","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:21] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"crm:sync-hubspot-objects","memoryBeforeCommandInMb":60.0,"memoryAfterCommandInMB":60.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"13c8d2de-a598-41db-b4ca-9799a4bee946","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:21] local.INFO: [SyncHubspotObjects] Starting sync {"team":"b2b115eb-93ce-4d1b-929c-173757df8fba","usage":24894088,"real_usage":65011712,"pid":62058} {"correlation_id":"81433689-3fcd-4128-8b21-068ce5ee75be","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:21] local.WARNING: [HubSpot] Account not connected for user {"userId":"33e34a7a-1c02-4f04-87ac-22c3a385e6e3","account":{"Jiminny\\Models\\SocialAccount":{"id":306,"sociable_id":109,"provider_user_id":"11348452","expires":1701077403,"refresh_token_expires":null,"provider":"hubspot","state":"full-refresh","auth_scope":null,"retry_after":null,"created_at":"2020-09-01 16:59:04","updated_at":"2023-11-27 09:30:03"}}} {"correlation_id":"81433689-3fcd-4128-8b21-068ce5ee75be","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:21] local.INFO: [CrmOwnerResolver] Integration owner is not connected, attempting team members {"crm_provider":"hubspot","crm_owner":109,"team_id":29} {"correlation_id":"81433689-3fcd-4128-8b21-068ce5ee75be","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:21] local.INFO: [CrmOwnerResolver] No team members found with active crm connection {"crm_provider":"hubspot","team_id":29} {"correlation_id":"81433689-3fcd-4128-8b21-068ce5ee75be","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:21] local.INFO: [CrmOwnerResolver] No team member found with active crm connection {"crm_provider":"hubspot","team_id":29} {"correlation_id":"81433689-3fcd-4128-8b21-068ce5ee75be","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:21] local.INFO: [SyncHubspotObjects] Sync finished {"team":"b2b115eb-93ce-4d1b-929c-173757df8fba","provider":"hubspot","status":"disconnected","duration_ms":35.85,"usage":24956128,"real_usage":65011712,"pid":62058,"reason":"Your HubSpot account has become disconnected. Please login to Jiminny to reconnect."} {"correlation_id":"81433689-3fcd-4128-8b21-068ce5ee75be","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:21] local.INFO: [SyncHubspotObjects] Starting sync {"team":"abae74b8-bfa8-4383-9a7f-89f4bf2bdbb4","usage":24914344,"real_usage":65011712,"pid":62058} {"correlation_id":"d2cb4aff-14c0-474f-89f2-f5c69dafe934","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:21] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1499,"provider":"hubspot"} {"correlation_id":"d2cb4aff-14c0-474f-89f2-f5c69dafe934","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:21] local.INFO: [SocialAccountService] Token retrieved {"socialAccountId":1499,"provider":"hubspot"} {"correlation_id":"d2cb4aff-14c0-474f-89f2-f5c69dafe934","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:21] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"d2cb4aff-14c0-474f-89f2-f5c69dafe934","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:21] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {"crm_provider":"hubspot","crm_owner":148,"team_id":2} {"correlation_id":"d2cb4aff-14c0-474f-89f2-f5c69dafe934","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:21] local.INFO: [HubSpot] Syncing opportunities using strategy: lastModified {"team":2} {"correlation_id":"d2cb4aff-14c0-474f-89f2-f5c69dafe934","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:21] local.INFO: [Hubspot] Pagination completed {"team_id":2,"endpoint":"https://api.hubapi.com/crm/v3/objects/deals/search","total_requests":1,"total_records_fetched":0,"total_elapsed_seconds":0.26,"average_seconds_per_request":0.26} {"correlation_id":"d2cb4aff-14c0-474f-89f2-f5c69dafe934","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:21] local.INFO: [HubSpot] Synced opportunities {"team":2,"strategies":"lastModified","sync_count":0,"total":0,"last_synced_id":null,"duration_ms":288.63} {"correlation_id":"d2cb4aff-14c0-474f-89f2-f5c69dafe934","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:21] local.INFO: [SyncHubspotObjects] Sync finished {"team":"abae74b8-bfa8-4383-9a7f-89f4bf2bdbb4","provider":"hubspot","status":"completed","duration_ms":324.99,"usage":24978696,"real_usage":65011712,"pid":62058} {"correlation_id":"d2cb4aff-14c0-474f-89f2-f5c69dafe934","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:22] local.INFO: [SyncHubspotObjects] Starting sync {"team":"c6b9d6b0-b48d-4832-a68c-a57d60651888","usage":24956624,"real_usage":65011712,"pid":62058} {"correlation_id":"955d9400-2f7f-4170-be31-43bafa1ef3bd","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:22] local.WARNING: [HubSpot] Account not connected for user {"userId":"71e3aac5-fb66-47c5-a236-2d051ae3e319","account":null} {"correlation_id":"955d9400-2f7f-4170-be31-43bafa1ef3bd","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:22] local.INFO: [CrmOwnerResolver] Integration owner is not connected, attempting team members {"crm_provider":"hubspot","crm_owner":256,"team_id":49} {"correlation_id":"955d9400-2f7f-4170-be31-43bafa1ef3bd","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:22] local.INFO: [CrmOwnerResolver] No team members found with active crm connection {"crm_provider":"hubspot","team_id":49} {"correlation_id":"955d9400-2f7f-4170-be31-43bafa1ef3bd","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:22] local.INFO: [CrmOwnerResolver] No team member found with active crm connection {"crm_provider":"hubspot","team_id":49} {"correlation_id":"955d9400-2f7f-4170-be31-43bafa1ef3bd","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:22] local.INFO: [SyncHubspotObjects] Sync finished {"team":"c6b9d6b0-b48d-4832-a68c-a57d60651888","provider":"hubspot","status":"disconnected","duration_ms":17.51,"usage":24941048,"real_usage":65011712,"pid":62058,"reason":"Social account for HubSpot cannot be found. Please login to Jiminny to connect."} {"correlation_id":"955d9400-2f7f-4170-be31-43bafa1ef3bd","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:22] local.INFO: [SyncHubspotObjects] Starting sync {"team":"b2d49a54-b645-4637-a7ae-a86cfce6e8e4","usage":24901688,"real_usage":65011712,"pid":62058} {"correlation_id":"6d0b5717-9160-4c2f-ad12-56e42a2c544a","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:22] local.WARNING: [HubSpot] Account not connected for user {"userId":"2ac0447f-3c8c-4ce0-baeb-b63ddb76fa9b","account":null} {"correlation_id":"6d0b5717-9160-4c2f-ad12-56e42a2c544a","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:22] local.INFO: [CrmOwnerResolver] Integration owner is not connected, attempting team members {"crm_provider":"hubspot","crm_owner":130,"team_id":42} {"correlation_id":"6d0b5717-9160-4c2f-ad12-56e42a2c544a","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:22] local.INFO: [CrmOwnerResolver] No team members found with active crm connection {"crm_provider":"hubspot","team_id":42} {"correlation_id":"6d0b5717-9160-4c2f-ad12-56e42a2c544a","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:22] local.INFO: [CrmOwnerResolver] No team member found with active crm connection {"crm_provider":"hubspot","team_id":42} {"correlation_id":"6d0b5717-9160-4c2f-ad12-56e42a2c544a","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:22] local.INFO: [SyncHubspotObjects] Sync finished {"team":"b2d49a54-b645-4637-a7ae-a86cfce6e8e4","provider":"hubspot","status":"disconnected","duration_ms":10.99,"usage":24944632,"real_usage":65011712,"pid":62058,"reason":"Social account for HubSpot cannot be found. Please login to Jiminny to connect."} {"correlation_id":"6d0b5717-9160-4c2f-ad12-56e42a2c544a","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:39] local.INFO: [HubSpot Journal Polling] No data {"correlation_id":"1f8f11e6-0cf5-49dd-b8d7-56f51ec27f8e","trace_id":"0f08076e-448d-487e-99a0-e73dae5c0ad4"}
[2026-05-11 10:41:39] local.WARNING: [HubSpot Journal Polling] Maximum empty results reached, stopping {"empty_results":5,"max_empty_results":5} {"correlation_id":"1f8f11e6-0cf5-49dd-b8d7-56f51ec27f8e","trace_id":"0f08076e-448d-487e-99a0-e73dae5c0ad4"}
[2026-05-11 10:41:39] local.WARNING: [HubSpot Journal Polling] Maximum empty results reached, stopping {"empty_results":5,"max_empty_results":5} {"correlation_id":"1f8f11e6-0cf5-49dd-b8d7-56f51ec27f8e","trace_id":"0f08076e-448d-487e-99a0-e73dae5c0ad4"}
[2026-05-11 10:41:39] local.INFO: [HubSpot Journal Polling] Service ending {"runtime_seconds":57,"total_cycles":5,"files_downloaded":0,"empty_files":0,"other_portal_skipped":0,"total_events":0,"events_per_file":0,"avg_api_ms":296.8,"avg_download_ms":0.0,"avg_transform_ms":0.0,"avg_process_ms":0.0,"peak_memory_mb":99.73} {"correlation_id":"1f8f11e6-0cf5-49dd-b8d7-56f51ec27f8e","trace_id":"0f08076e-448d-487e-99a0-e73dae5c0ad4"}
[2026-05-11 10:41:39] local.INFO: [HubSpot Journal Polling] Saved offset to database on cleanup {"offset":"019e15a9-9ea0-7da7-87bc-82592e3ccf0d"} {"correlation_id":"1f8f11e6-0cf5-49dd-b8d7-56f51ec27f8e","trace_id":"0f08076e-448d-487e-99a0-e73dae5c0ad4"}
[2026-05-11 10:41:39] local.INFO: [HubSpot Journal Polling] Released polling lock {"correlation_id":"1f8f11e6-0cf5-49dd-b8d7-56f51ec27f8e","trace_id":"0f08076e-448d-487e-99a0-e73dae5c0ad4"}
[2026-05-11 10:42:07] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"meeting-bot:schedule-bot","memoryBeforeCommandInMb":60.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"1aa66921-0a71-4d61-8cd7-0663242dca44","trace_id":"f6b1d739-39b6-4043-a81e-a1af2c76d401"}
[2026-05-11 10:42:07] local.INFO: [ScheduleBotCommand] Number of activities to be captured: 0 {"correlation_id":"1aa66921-0a71-4d61-8cd7-0663242dca44","trace_id":"f6b1d739-39b6-4043-a81e-a1af2c76d401"}
[2026-05-11 10:42:07] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"meeting-bot:schedule-bot","memoryBeforeCommandInMb":60.0,"memoryAfterCommandInMB":60.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"1aa66921-0a71-4d61-8cd7-0663242dca44","trace_id":"f6b1d739-39b6-4043-a81e-a1af2c76d401"}
[2026-05-11 10:42:10] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"dialers:monitor-activities","memoryBeforeCommandInMb":60.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"cc420ba5-39bf-4183-95ae-48962a9c124b","trace_id":"8daf6081-5c8a-4fae-98d8-e317d8580bc3"}
[2026-05-11 10:42:10] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"dialers:monitor-activities","memoryBeforeCommandInMb":60.0,"memoryAfterCommandInMB":60.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"cc420ba5-39bf-4183-95ae-48962a9c124b","trace_id":"8daf6081-5c8a-4fae-98d8-e317d8580bc3"}
[2026-05-11 10:42:13] local.NOTICE: Monitoring start {"correlation_id":"50696062-587c-4c43-8075-7229916ea46f","trace_id":"b58f754c-906f-413b-80e6-f89189dbe078"}
[2026-05-11 10:42:13] local.NOTICE: Monitoring end {"correlation_id":"50696062-587c-4c43-8075-7229916ea46f","trace_id":"b58f754c-906f-413b-80e6-f89189dbe078"}
[2026-05-11 10:42:17] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"mailbox:skip-lists:refresh","memoryBeforeCommandInMb":60.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"f70656cd-6de3-4f8b-bb9d-74a941c552c4","trace_id":"c780d8e3-2abd-41a3-a9dc-7c2be6aa22d4"}
[2026-05-11 10:42:17] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"mailbox:skip-lists:refresh","memoryBeforeCommandInMb":60.0,"memoryAfterCommandInMB":60.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"f70656cd-6de3-4f8b-bb9d-74a941c552c4","trace_id":"c780d8e3-2abd-41a3-a9dc-7c2be6aa22d4"}
[2026-05-11 10:42:20] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"mailbox:batch:process","memoryBeforeCommandInMb":60.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"b2e73601-1777-4ddb-a152-fe85f4c89afc","trace_id":"8bac5054-216a-4605-b13b-9efab8ab5341"}
[2026-05-11 10:42:20] local.INFO: [EmailSchedule] STARTING batch process {"host":"docker_lamp_1"} {"correlation_id":"b2e73601-1777-4ddb-a152-fe85f4c89afc","trace_id":"8bac5054-216a-4605-b13b-9efab8ab5341"}
[2026-05-11 10:42:20] local.INFO: [EmailSchedule] FINISHED batch process {"host":"docker_lamp_1","processed":0} {"correlation_i...
|
17968
|
NULL
|
NULL
|
NULL
|
|
17971
|
782
|
11
|
2026-05-11T10:43:50.313696+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778496230313_m1.jpg...
|
PhpStorm
|
faVsco.js – laravel.log
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
3
70
3
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\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 Illuminate\Support\Facades\Redis;
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)
{
$cacheKey = $this->getRateLimitCacheKey();
$cachedRetryAfter = Redis::get($cacheKey);
if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {
throw new RateLimitException(
'Hubspot rate limit (cached circuit-breaker)',
(int) $cachedRetryAfter,
);
}
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'policy' => $this->parsePolicy($e),
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function getRateLimitCacheKey(): string
{
return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());
}
public function isHubspotRateLimit(Throwable $e): bool
{
if ($e instanceof BadRequest
|| $e instanceof DealApiException
|| $e instanceof ContactApiException
|| $e instanceof CompanyApiException
|| $e instanceof \GuzzleHttp\Exception\RequestException
) {
return (int) $e->getCode() === 429;
}
return false;
}
public function parseRetryAfter(Throwable $e): int
{
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$e ' . PHP_EOL . print_r($e, true));
return 5;
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;
}
}
$policy = $this->parsePolicy($e);
if ($policy === 'TEN_SECONDLY_ROLLING') {
return 10;
}
if ($policy === 'SECONDLY') {
return 1;
}
if ($policy === 'DAILY_LIMIT') {
return 600;
}
$this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [
'exception_class' => get_class($e),
]);
return 10;
}
public function parsePolicy(Throwable $e): ?string
{
if (! method_exists($e, 'getResponseBody')) {
return null;
}
$body = $e->getResponseBody();
if (is_string($body)) {
$body = json_decode($body, true) ?? [];
}
if (! is_array($body)) {
return null;
}
$policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;
return is_string($policy) ? strtoupper($policy) : null;
}
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
);
}
/**
* Execute a search request against HubSpot CRM objects with rate limiting.
*
* @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')
* @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.
* @return array The search response with 'results', 'total', 'paging' keys
* @throws RateLimitException When rate limit is hit
* @throws HubspotException On API errors
*/
public function search(string $objectType, array $payload): array
{
$endpoint = self::BASE_URL . "/crm/v3/objects/{$objectType}/search";
return $this->executeRequest(function () use ($endpoint, $payload) {
$response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$response ' . PHP_EOL . print_r($response, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$headers ' . PHP_EOL . print_r($response->getHeaders(), true));
return $response->toArray();
});
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$deal = $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') {
return $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
return $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
}
/**
* @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 (RateLimitException $e) {
// throw $e;
} 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);
}
}...
|
[{"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":"JY-20725-handle-HS-search-rate-limit, menu","depth":5,"on_screen":true,"help_text":"Git Branch: JY-20725-handle-HS-search-rate-limit","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":"HandleHubspotRateLimitTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'HandleHubspotRateLimitTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'HandleHubspotRateLimitTest'","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":"AXStaticText","text":"3","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"70","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"3","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"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\\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 Illuminate\\Support\\Facades\\Redis;\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\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Reacts to a rate limits (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 $cacheKey = $this->getRateLimitCacheKey();\n\n $cachedRetryAfter = Redis::get($cacheKey);\n if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n (int) $cachedRetryAfter,\n );\n }\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);\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 'policy' => $this->parsePolicy($e),\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 getRateLimitCacheKey(): string\n {\n return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());\n }\n\n public function isHubspotRateLimit(Throwable $e): bool\n {\n if ($e instanceof BadRequest\n || $e instanceof DealApiException\n || $e instanceof ContactApiException\n || $e instanceof CompanyApiException\n || $e instanceof \\GuzzleHttp\\Exception\\RequestException\n ) {\n return (int) $e->getCode() === 429;\n }\n\n return false;\n }\n\n public function parseRetryAfter(Throwable $e): int\n {\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$e ' . PHP_EOL . print_r($e, true));\n return 5;\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 $policy = $this->parsePolicy($e);\n if ($policy === 'TEN_SECONDLY_ROLLING') {\n return 10;\n }\n if ($policy === 'SECONDLY') {\n return 1;\n }\n if ($policy === 'DAILY_LIMIT') {\n return 600;\n }\n\n $this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [\n 'exception_class' => get_class($e),\n ]);\n\n return 10;\n }\n\n public function parsePolicy(Throwable $e): ?string\n {\n if (! method_exists($e, 'getResponseBody')) {\n return null;\n }\n\n $body = $e->getResponseBody();\n if (is_string($body)) {\n $body = json_decode($body, true) ?? [];\n }\n\n if (! is_array($body)) {\n return null;\n }\n\n $policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;\n\n return is_string($policy) ? strtoupper($policy) : null;\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 * Execute a search request against HubSpot CRM objects with rate limiting.\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n * @return array The search response with 'results', 'total', 'paging' keys\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n */\n public function search(string $objectType, array $payload): array\n {\n $endpoint = self::BASE_URL . \"/crm/v3/objects/{$objectType}/search\";\n\n return $this->executeRequest(function () use ($endpoint, $payload) {\n $response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$response ' . PHP_EOL . print_r($response, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$headers ' . PHP_EOL . print_r($response->getHeaders(), true));\n\n return $response->toArray();\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 $deal = $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 return $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n return $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\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 (RateLimitException $e) {\n// throw $e;\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\\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 Illuminate\\Support\\Facades\\Redis;\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\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Reacts to a rate limits (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 $cacheKey = $this->getRateLimitCacheKey();\n\n $cachedRetryAfter = Redis::get($cacheKey);\n if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n (int) $cachedRetryAfter,\n );\n }\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);\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 'policy' => $this->parsePolicy($e),\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 getRateLimitCacheKey(): string\n {\n return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());\n }\n\n public function isHubspotRateLimit(Throwable $e): bool\n {\n if ($e instanceof BadRequest\n || $e instanceof DealApiException\n || $e instanceof ContactApiException\n || $e instanceof CompanyApiException\n || $e instanceof \\GuzzleHttp\\Exception\\RequestException\n ) {\n return (int) $e->getCode() === 429;\n }\n\n return false;\n }\n\n public function parseRetryAfter(Throwable $e): int\n {\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$e ' . PHP_EOL . print_r($e, true));\n return 5;\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 $policy = $this->parsePolicy($e);\n if ($policy === 'TEN_SECONDLY_ROLLING') {\n return 10;\n }\n if ($policy === 'SECONDLY') {\n return 1;\n }\n if ($policy === 'DAILY_LIMIT') {\n return 600;\n }\n\n $this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [\n 'exception_class' => get_class($e),\n ]);\n\n return 10;\n }\n\n public function parsePolicy(Throwable $e): ?string\n {\n if (! method_exists($e, 'getResponseBody')) {\n return null;\n }\n\n $body = $e->getResponseBody();\n if (is_string($body)) {\n $body = json_decode($body, true) ?? [];\n }\n\n if (! is_array($body)) {\n return null;\n }\n\n $policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;\n\n return is_string($policy) ? strtoupper($policy) : null;\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 * Execute a search request against HubSpot CRM objects with rate limiting.\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n * @return array The search response with 'results', 'total', 'paging' keys\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n */\n public function search(string $objectType, array $payload): array\n {\n $endpoint = self::BASE_URL . \"/crm/v3/objects/{$objectType}/search\";\n\n return $this->executeRequest(function () use ($endpoint, $payload) {\n $response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$response ' . PHP_EOL . print_r($response, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$headers ' . PHP_EOL . print_r($response->getHeaders(), true));\n\n return $response->toArray();\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 $deal = $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 return $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n return $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\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 (RateLimitException $e) {\n// throw $e;\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}]...
|
-6321601233329783344
|
5225697106764630116
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
3
70
3
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\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 Illuminate\Support\Facades\Redis;
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)
{
$cacheKey = $this->getRateLimitCacheKey();
$cachedRetryAfter = Redis::get($cacheKey);
if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {
throw new RateLimitException(
'Hubspot rate limit (cached circuit-breaker)',
(int) $cachedRetryAfter,
);
}
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'policy' => $this->parsePolicy($e),
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function getRateLimitCacheKey(): string
{
return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());
}
public function isHubspotRateLimit(Throwable $e): bool
{
if ($e instanceof BadRequest
|| $e instanceof DealApiException
|| $e instanceof ContactApiException
|| $e instanceof CompanyApiException
|| $e instanceof \GuzzleHttp\Exception\RequestException
) {
return (int) $e->getCode() === 429;
}
return false;
}
public function parseRetryAfter(Throwable $e): int
{
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$e ' . PHP_EOL . print_r($e, true));
return 5;
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;
}
}
$policy = $this->parsePolicy($e);
if ($policy === 'TEN_SECONDLY_ROLLING') {
return 10;
}
if ($policy === 'SECONDLY') {
return 1;
}
if ($policy === 'DAILY_LIMIT') {
return 600;
}
$this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [
'exception_class' => get_class($e),
]);
return 10;
}
public function parsePolicy(Throwable $e): ?string
{
if (! method_exists($e, 'getResponseBody')) {
return null;
}
$body = $e->getResponseBody();
if (is_string($body)) {
$body = json_decode($body, true) ?? [];
}
if (! is_array($body)) {
return null;
}
$policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;
return is_string($policy) ? strtoupper($policy) : null;
}
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
);
}
/**
* Execute a search request against HubSpot CRM objects with rate limiting.
*
* @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')
* @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.
* @return array The search response with 'results', 'total', 'paging' keys
* @throws RateLimitException When rate limit is hit
* @throws HubspotException On API errors
*/
public function search(string $objectType, array $payload): array
{
$endpoint = self::BASE_URL . "/crm/v3/objects/{$objectType}/search";
return $this->executeRequest(function () use ($endpoint, $payload) {
$response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$response ' . PHP_EOL . print_r($response, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$headers ' . PHP_EOL . print_r($response->getHeaders(), true));
return $response->toArray();
});
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$deal = $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') {
return $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
return $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
}
/**
* @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 (RateLimitException $e) {
// throw $e;
} 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);
}
}...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
17972
|
782
|
12
|
2026-05-11T10:43:51.977702+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-11/1778 /Users/lukas/.screenpipe/data/data/2026-05-11/1778496231977_m1.jpg...
|
PhpStorm
|
faVsco.js – laravel.log
|
1
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
3
70
3
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\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 Illuminate\Support\Facades\Redis;
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)
{
$cacheKey = $this->getRateLimitCacheKey();
$cachedRetryAfter = Redis::get($cacheKey);
if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {
throw new RateLimitException(
'Hubspot rate limit (cached circuit-breaker)',
(int) $cachedRetryAfter,
);
}
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'policy' => $this->parsePolicy($e),
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function getRateLimitCacheKey(): string
{
return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());
}
public function isHubspotRateLimit(Throwable $e): bool
{
if ($e instanceof BadRequest
|| $e instanceof DealApiException
|| $e instanceof ContactApiException
|| $e instanceof CompanyApiException
|| $e instanceof \GuzzleHttp\Exception\RequestException
) {
return (int) $e->getCode() === 429;
}
return false;
}
public function parseRetryAfter(Throwable $e): int
{
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$e ' . PHP_EOL . print_r($e, true));
return 5;
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;
}
}
$policy = $this->parsePolicy($e);
if ($policy === 'TEN_SECONDLY_ROLLING') {
return 10;
}
if ($policy === 'SECONDLY') {
return 1;
}
if ($policy === 'DAILY_LIMIT') {
return 600;
}
$this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [
'exception_class' => get_class($e),
]);
return 10;
}
public function parsePolicy(Throwable $e): ?string
{
if (! method_exists($e, 'getResponseBody')) {
return null;
}
$body = $e->getResponseBody();
if (is_string($body)) {
$body = json_decode($body, true) ?? [];
}
if (! is_array($body)) {
return null;
}
$policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;
return is_string($policy) ? strtoupper($policy) : null;
}
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
);
}
/**
* Execute a search request against HubSpot CRM objects with rate limiting.
*
* @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')
* @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.
* @return array The search response with 'results', 'total', 'paging' keys
* @throws RateLimitException When rate limit is hit
* @throws HubspotException On API errors
*/
public function search(string $objectType, array $payload): array
{
$endpoint = self::BASE_URL . "/crm/v3/objects/{$objectType}/search";
return $this->executeRequest(function () use ($endpoint, $payload) {
$response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$response ' . PHP_EOL . print_r($response, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$headers ' . PHP_EOL . print_r($response->getHeaders(), true));
return $response->toArray();
});
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$deal = $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') {
return $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
return $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
}
/**
* @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 (RateLimitException $e) {
// throw $e;
} 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);
}
}
Show Replace Field
Search History
Received 429 from API
New Line
Match Case
Words
Regex
Replace History
Replace
New Line
Preserve case
0 results
Previous Occurrence
Next Occurrence
Filter Search Results
Open in Window, Multiple Cursors
Click to highlight
Close
Sync Changes
Hide This Notification
Code changed:
Hide
[2026-05-11 10:41:16] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"mailbox:batch:process","memoryBeforeCommandInMb":60.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"ee9b9096-b1f1-4e2b-ac17-af0003eb00c0","trace_id":"8904c61b-d5b4-44cc-91bf-68f0f73222c0"}
[2026-05-11 10:41:16] local.INFO: [EmailSchedule] STARTING batch process {"host":"docker_lamp_1"} {"correlation_id":"ee9b9096-b1f1-4e2b-ac17-af0003eb00c0","trace_id":"8904c61b-d5b4-44cc-91bf-68f0f73222c0"}
[2026-05-11 10:41:17] local.INFO: [EmailSchedule] FINISHED batch process {"host":"docker_lamp_1","processed":0} {"correlation_id":"ee9b9096-b1f1-4e2b-ac17-af0003eb00c0","trace_id":"8904c61b-d5b4-44cc-91bf-68f0f73222c0"}
[2026-05-11 10:41:17] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"mailbox:batch:process","memoryBeforeCommandInMb":60.0,"memoryAfterCommandInMB":60.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"ee9b9096-b1f1-4e2b-ac17-af0003eb00c0","trace_id":"8904c61b-d5b4-44cc-91bf-68f0f73222c0"}
[2026-05-11 10:41:21] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"crm:sync-hubspot-objects","memoryBeforeCommandInMb":60.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"13c8d2de-a598-41db-b4ca-9799a4bee946","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:21] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"crm:sync-hubspot-objects","memoryBeforeCommandInMb":60.0,"memoryAfterCommandInMB":60.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"13c8d2de-a598-41db-b4ca-9799a4bee946","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:21] local.INFO: [SyncHubspotObjects] Starting sync {"team":"b2b115eb-93ce-4d1b-929c-173757df8fba","usage":24894088,"real_usage":65011712,"pid":62058} {"correlation_id":"81433689-3fcd-4128-8b21-068ce5ee75be","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:21] local.WARNING: [HubSpot] Account not connected for user {"userId":"33e34a7a-1c02-4f04-87ac-22c3a385e6e3","account":{"Jiminny\\Models\\SocialAccount":{"id":306,"sociable_id":109,"provider_user_id":"11348452","expires":1701077403,"refresh_token_expires":null,"provider":"hubspot","state":"full-refresh","auth_scope":null,"retry_after":null,"created_at":"2020-09-01 16:59:04","updated_at":"2023-11-27 09:30:03"}}} {"correlation_id":"81433689-3fcd-4128-8b21-068ce5ee75be","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:21] local.INFO: [CrmOwnerResolver] Integration owner is not connected, attempting team members {"crm_provider":"hubspot","crm_owner":109,"team_id":29} {"correlation_id":"81433689-3fcd-4128-8b21-068ce5ee75be","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:21] local.INFO: [CrmOwnerResolver] No team members found with active crm connection {"crm_provider":"hubspot","team_id":29} {"correlation_id":"81433689-3fcd-4128-8b21-068ce5ee75be","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:21] local.INFO: [CrmOwnerResolver] No team member found with active crm connection {"crm_provider":"hubspot","team_id":29} {"correlation_id":"81433689-3fcd-4128-8b21-068ce5ee75be","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:21] local.INFO: [SyncHubspotObjects] Sync finished {"team":"b2b115eb-93ce-4d1b-929c-173757df8fba","provider":"hubspot","status":"disconnected","duration_ms":35.85,"usage":24956128,"real_usage":65011712,"pid":62058,"reason":"Your HubSpot account has become disconnected. Please login to Jiminny to reconnect."} {"correlation_id":"81433689-3fcd-4128-8b21-068ce5ee75be","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:21] local.INFO: [SyncHubspotObjects] Starting sync {"team":"abae74b8-bfa8-4383-9a7f-89f4bf2bdbb4","usage":24914344,"real_usage":65011712,"pid":62058} {"correlation_id":"d2cb4aff-14c0-474f-89f2-f5c69dafe934","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:21] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1499,"provider":"hubspot"} {"correlation_id":"d2cb4aff-14c0-474f-89f2-f5c69dafe934","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:21] local.INFO: [SocialAccountService] Token retrieved {"socialAccountId":1499,"provider":"hubspot"} {"correlation_id":"d2cb4aff-14c0-474f-89f2-f5c69dafe934","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:21] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"d2cb4aff-14c0-474f-89f2-f5c69dafe934","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:21] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {"crm_provider":"hubspot","crm_owner":148,"team_id":2} {"correlation_id":"d2cb4aff-14c0-474f-89f2-f5c69dafe934","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:21] local.INFO: [HubSpot] Syncing opportunities using strategy: lastModified {"team":2} {"correlation_id":"d2cb4aff-14c0-474f-89f2-f5c69dafe934","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:21] local.INFO: [Hubspot] Pagination completed {"team_id":2,"endpoint":"https://api.hubapi.com/crm/v3/objects/deals/search","total_requests":1,"total_records_fetched":0,"total_elapsed_seconds":0.26,"average_seconds_per_request":0.26} {"correlation_id":"d2cb4aff-14c0-474f-89f2-f5c69dafe934","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:21] local.INFO: [HubSpot] Synced opportunities {"team":2,"strategies":"lastModified","sync_count":0,"total":0,"last_synced_id":null,"duration_ms":288.63} {"correlation_id":"d2cb4aff-14c0-474f-89f2-f5c69dafe934","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:21] local.INFO: [SyncHubspotObjects] Sync finished {"team":"abae74b8-bfa8-4383-9a7f-89f4bf2bdbb4","provider":"hubspot","status":"completed","duration_ms":324.99,"usage":24978696,"real_usage":65011712,"pid":62058} {"correlation_id":"d2cb4aff-14c0-474f-89f2-f5c69dafe934","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:22] local.INFO: [SyncHubspotObjects] Starting sync {"team":"c6b9d6b0-b48d-4832-a68c-a57d60651888","usage":24956624,"real_usage":65011712,"pid":62058} {"correlation_id":"955d9400-2f7f-4170-be31-43bafa1ef3bd","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:22] local.WARNING: [HubSpot] Account not connected for user {"userId":"71e3aac5-fb66-47c5-a236-2d051ae3e319","account":null} {"correlation_id":"955d9400-2f7f-4170-be31-43bafa1ef3bd","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:22] local.INFO: [CrmOwnerResolver] Integration owner is not connected, attempting team members {"crm_provider":"hubspot","crm_owner":256,"team_id":49} {"correlation_id":"955d9400-2f7f-4170-be31-43bafa1ef3bd","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:22] local.INFO: [CrmOwnerResolver] No team members found with active crm connection {"crm_provider":"hubspot","team_id":49} {"correlation_id":"955d9400-2f7f-4170-be31-43bafa1ef3bd","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:22] local.INFO: [CrmOwnerResolver] No team member found with active crm connection {"crm_provider":"hubspot","team_id":49} {"correlation_id":"955d9400-2f7f-4170-be31-43bafa1ef3bd","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:22] local.INFO: [SyncHubspotObjects] Sync finished {"team":"c6b9d6b0-b48d-4832-a68c-a57d60651888","provider":"hubspot","status":"disconnected","duration_ms":17.51,"usage":24941048,"real_usage":65011712,"pid":62058,"reason":"Social account for HubSpot cannot be found. Please login to Jiminny to connect."} {"correlation_id":"955d9400-2f7f-4170-be31-43bafa1ef3bd","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:22] local.INFO: [SyncHubspotObjects] Starting sync {"team":"b2d49a54-b645-4637-a7ae-a86cfce6e8e4","usage":24901688,"real_usage":65011712,"pid":62058} {"correlation_id":"6d0b5717-9160-4c2f-ad12-56e42a2c544a","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:22] local.WARNING: [HubSpot] Account not connected for user {"userId":"2ac0447f-3c8c-4ce0-baeb-b63ddb76fa9b","account":null} {"correlation_id":"6d0b5717-9160-4c2f-ad12-56e42a2c544a","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:22] local.INFO: [CrmOwnerResolver] Integration owner is not connected, attempting team members {"crm_provider":"hubspot","crm_owner":130,"team_id":42} {"correlation_id":"6d0b5717-9160-4c2f-ad12-56e42a2c544a","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:22] local.INFO: [CrmOwnerResolver] No team members found with active crm connection {"crm_provider":"hubspot","team_id":42} {"correlation_id":"6d0b5717-9160-4c2f-ad12-56e42a2c544a","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:22] local.INFO: [CrmOwnerResolver] No team member found with active crm connection {"crm_provider":"hubspot","team_id":42} {"correlation_id":"6d0b5717-9160-4c2f-ad12-56e42a2c544a","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:22] local.INFO: [SyncHubspotObjects] Sync finished {"team":"b2d49a54-b645-4637-a7ae-a86cfce6e8e4","provider":"hubspot","status":"disconnected","duration_ms":10.99,"usage":24944632,"real_usage":65011712,"pid":62058,"reason":"Social account for HubSpot cannot be found. Please login to Jiminny to connect."} {"correlation_id":"6d0b5717-9160-4c2f-ad12-56e42a2c544a","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:39] local.INFO: [HubSpot Journal Polling] No data {"correlation_id":"1f8f11e6-0cf5-49dd-b8d7-56f51ec27f8e","trace_id":"0f08076e-448d-487e-99a0-e73dae5c0ad4"}
[2026-05-11 10:41:39] local.WARNING: [HubSpot Journal Polling] Maximum empty results reached, stopping {"empty_results":5,"max_empty_results":5} {"correlation_id":"1f8f11e6-0cf5-49dd-b8d7-56f51ec27f8e","trace_id":"0f08076e-448d-487e-99a0-e73dae5c0ad4"}
[2026-05-11 10:41:39] local.WARNING: [HubSpot Journal Polling] Maximum empty results reached, stopping {"empty_results":5,"max_empty_results":5} {"correlation_id":"1f8f11e6-0cf5-49dd-b8d7-56f51ec27f8e","trace_id":"0f08076e-448d-487e-99a0-e73dae5c0ad4"}
[2026-05-11 10:41:39] local.INFO: [HubSpot Journal Polling] Service ending {"runtime_seconds":57,"total_cycles":5,"files_downloaded":0,"empty_files":0,"other_portal_skipped":0,"total_events":0,"events_per_file":0,"avg_api_ms":296.8,"avg_download_ms":0.0,"avg_transform_ms":0.0,"avg_process_ms":0.0,"peak_memory_mb":99.73} {"correlation_id":"1f8f11e6-0cf5-49dd-b8d7-56f51ec27f8e","trace_id":"0f08076e-448d-487e-99a0-e73dae5c0ad4"}
[2026-05-11 10:41:39] local.INFO: [HubSpot Journal Polling] Saved offset to database on cleanup {"offset":"019e15a9-9ea0-7da7-87bc-82592e3ccf0d"} {"correlation_id":"1f8f11e6-0cf5-49dd-b8d7-56f51ec27f8e","trace_id":"0f08076e-448d-487e-99a0-e73dae5c0ad4"}
[2026-05-11 10:41:39] local.INFO: [HubSpot Journal Polling] Released polling lock {"correlation_id":"1f8f11e6-0cf5-49dd-b8d7-56f51ec27f8e","trace_id":"0f08076e-448d-487e-99a0-e73dae5c0ad4"}
[2026-05-11 10:42:07] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"meeting-bot:schedule-bot","memoryBeforeCommandInMb":60.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"1aa66921-0a71-4d61-8cd7-0663242dca44","trace_id":"f6b1d739-39b6-4043-a81e-a1af2c76d401"}
[2026-05-11 10:42:07] local.INFO: [ScheduleBotCommand] Number of activities to be captured: 0 {"correlation_id":"1aa66921-0a71-4d61-8cd7-0663242dca44","trace_id":"f6b1d739-39b6-4043-a81e-a1af2c76d401"}
[2026-05-11 10:42:07] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"meeting-bot:schedule-bot","memoryBeforeCommandInMb":60.0,"memoryAfterCommandInMB":60.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"1aa66921-0a71-4d61-8cd7-0663242dca44","trace_id":"f6b1d739-39b6-4043-a81e-a1af2c76d401"}
[2026-05-11 10:42:10] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"dialers:monitor-activities","memoryBeforeCommandInMb":60.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"cc420ba5-39bf-4183-95ae-48962a9c124b","trace_id":"8daf6081-5c8a-4fae-98d8-e317d8580bc3"}
[2026-05-11 10:42:10] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"dialers:monitor-activities","memoryBeforeCommandInMb":60.0,"memoryAfterCommandInMB":60.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"cc420ba5-39bf-4183-95ae-48962a9c124b","trace_id":"8daf6081-5c8a-4fae-98d8-e317d8580bc3"}
[2026-05-11 10:42:13] local.NOTICE: Monitoring start {"correlation_id":"50696062-587c-4c43-8075-7229916ea46f","trace_id":"b58f754c-906f-413b-80e6-f89189dbe078"}
[2026-05-11 10:42:13] local.NOTICE: Monitoring end {"correlation_id":"50696062-587c-4c43-8075-7229916ea46f","trace_id":"b58f754c-906f-413b-80e6-f89189dbe078"}
[2026-05-11 10:42:17] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"mailbox:skip-lists:refresh","memoryBeforeCommandInMb":60.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"f70656cd-6de3-4f8b-bb9d-74a941c552c4","trace_id":"c780d8e3-2abd-41a3-a9dc-7c2be6aa22d4"}
[2026-05-11 10:42:17] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"mailbox:skip-lists:refresh","memoryBeforeCommandInMb":60.0,"memoryAfterCommandInMB":60.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"f70656cd-6de3-4f8b-bb9d-74a941c552c4","trace_id":"c780d8e3-2abd-41a3-a9dc-7c2be6aa22d4"}
[2026-05-11 10:42:20] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"mailbox:batch:process","memoryBeforeCommandInMb":60.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"b2e73601-1777-4ddb-a152-fe85f4c89afc","trace_id":"8bac5054-216a-4605-b13b-9efab8ab5341"}
[2026-05-11 10:42:20] local.INFO: [EmailSchedule] STARTING batch process {"host":"docker_lamp_1"} {"correlation_id":"b2e73601-1777-4ddb-a152-fe85f4c89afc","trace_id":"8bac5054-216a-4605-b13b-9efab8ab5341"}
[2026-05-11 10:42:20] local.INFO: [EmailSchedule] FINISHED batch process {"host":"docker_lamp_1","processed":0} {"correlation_i...
|
[{"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":"JY-20725-handle-HS-search-rate-limit, menu","depth":5,"on_screen":true,"help_text":"Git Branch: JY-20725-handle-HS-search-rate-limit","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":"HandleHubspotRateLimitTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'HandleHubspotRateLimitTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'HandleHubspotRateLimitTest'","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":"AXStaticText","text":"3","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"70","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"3","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"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\\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 Illuminate\\Support\\Facades\\Redis;\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\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Reacts to a rate limits (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 $cacheKey = $this->getRateLimitCacheKey();\n\n $cachedRetryAfter = Redis::get($cacheKey);\n if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n (int) $cachedRetryAfter,\n );\n }\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);\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 'policy' => $this->parsePolicy($e),\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 getRateLimitCacheKey(): string\n {\n return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());\n }\n\n public function isHubspotRateLimit(Throwable $e): bool\n {\n if ($e instanceof BadRequest\n || $e instanceof DealApiException\n || $e instanceof ContactApiException\n || $e instanceof CompanyApiException\n || $e instanceof \\GuzzleHttp\\Exception\\RequestException\n ) {\n return (int) $e->getCode() === 429;\n }\n\n return false;\n }\n\n public function parseRetryAfter(Throwable $e): int\n {\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$e ' . PHP_EOL . print_r($e, true));\n return 5;\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 $policy = $this->parsePolicy($e);\n if ($policy === 'TEN_SECONDLY_ROLLING') {\n return 10;\n }\n if ($policy === 'SECONDLY') {\n return 1;\n }\n if ($policy === 'DAILY_LIMIT') {\n return 600;\n }\n\n $this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [\n 'exception_class' => get_class($e),\n ]);\n\n return 10;\n }\n\n public function parsePolicy(Throwable $e): ?string\n {\n if (! method_exists($e, 'getResponseBody')) {\n return null;\n }\n\n $body = $e->getResponseBody();\n if (is_string($body)) {\n $body = json_decode($body, true) ?? [];\n }\n\n if (! is_array($body)) {\n return null;\n }\n\n $policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;\n\n return is_string($policy) ? strtoupper($policy) : null;\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 * Execute a search request against HubSpot CRM objects with rate limiting.\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n * @return array The search response with 'results', 'total', 'paging' keys\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n */\n public function search(string $objectType, array $payload): array\n {\n $endpoint = self::BASE_URL . \"/crm/v3/objects/{$objectType}/search\";\n\n return $this->executeRequest(function () use ($endpoint, $payload) {\n $response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$response ' . PHP_EOL . print_r($response, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$headers ' . PHP_EOL . print_r($response->getHeaders(), true));\n\n return $response->toArray();\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 $deal = $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 return $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n return $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\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 (RateLimitException $e) {\n// throw $e;\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\\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 Illuminate\\Support\\Facades\\Redis;\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\n public function __construct(\n SocialAccountService $socialAccountService,\n HubspotPaginationService $paginationService,\n HubspotTokenManager $tokenManager\n ) {\n parent::__construct($socialAccountService);\n $this->paginationService = $paginationService;\n $this->tokenManager = $tokenManager;\n\n $this->setBaseUrl(self::BASE_URL);\n $this->setVersion(self::MIN_API_VERSION);\n }\n\n /**\n * Reacts to a rate limits (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 $cacheKey = $this->getRateLimitCacheKey();\n\n $cachedRetryAfter = Redis::get($cacheKey);\n if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {\n throw new RateLimitException(\n 'Hubspot rate limit (cached circuit-breaker)',\n (int) $cachedRetryAfter,\n );\n }\n\n try {\n return $apiCall();\n } catch (Throwable $e) {\n if ($this->isHubspotRateLimit($e)) {\n $retryAfter = $this->parseRetryAfter($e);\n\n Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);\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 'policy' => $this->parsePolicy($e),\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 getRateLimitCacheKey(): string\n {\n return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());\n }\n\n public function isHubspotRateLimit(Throwable $e): bool\n {\n if ($e instanceof BadRequest\n || $e instanceof DealApiException\n || $e instanceof ContactApiException\n || $e instanceof CompanyApiException\n || $e instanceof \\GuzzleHttp\\Exception\\RequestException\n ) {\n return (int) $e->getCode() === 429;\n }\n\n return false;\n }\n\n public function parseRetryAfter(Throwable $e): int\n {\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$e ' . PHP_EOL . print_r($e, true));\n return 5;\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 $policy = $this->parsePolicy($e);\n if ($policy === 'TEN_SECONDLY_ROLLING') {\n return 10;\n }\n if ($policy === 'SECONDLY') {\n return 1;\n }\n if ($policy === 'DAILY_LIMIT') {\n return 600;\n }\n\n $this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [\n 'exception_class' => get_class($e),\n ]);\n\n return 10;\n }\n\n public function parsePolicy(Throwable $e): ?string\n {\n if (! method_exists($e, 'getResponseBody')) {\n return null;\n }\n\n $body = $e->getResponseBody();\n if (is_string($body)) {\n $body = json_decode($body, true) ?? [];\n }\n\n if (! is_array($body)) {\n return null;\n }\n\n $policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;\n\n return is_string($policy) ? strtoupper($policy) : null;\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 * Execute a search request against HubSpot CRM objects with rate limiting.\n *\n * @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')\n * @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.\n * @return array The search response with 'results', 'total', 'paging' keys\n * @throws RateLimitException When rate limit is hit\n * @throws HubspotException On API errors\n */\n public function search(string $objectType, array $payload): array\n {\n $endpoint = self::BASE_URL . \"/crm/v3/objects/{$objectType}/search\";\n\n return $this->executeRequest(function () use ($endpoint, $payload) {\n $response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$response ' . PHP_EOL . print_r($response, true));\n \\Illuminate\\Support\\Facades\\Log::channel('custom_channel')->info('$headers ' . PHP_EOL . print_r($response->getHeaders(), true));\n\n return $response->toArray();\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 $deal = $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 return $this->getInstance()->getClient()?->request(\n method: $method,\n endpoint: $endpoint,\n query_string: $queryString\n );\n } else {\n return $this->getInstance()->getClient()->request($method, $endpoint, [\n 'json' => ($payload),\n ]);\n }\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 (RateLimitException $e) {\n// throw $e;\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":"AXButton","text":"Show Replace Field","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Search History","depth":3,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"Received 429 from API","depth":4,"on_screen":true,"value":"Received 429 from API","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"New Line","depth":3,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Match Case","depth":3,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Words","depth":3,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Regex","depth":3,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Replace History","depth":3,"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":4,"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":3,"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":3,"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":"AXStaticText","text":"0 results","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Occurrence","depth":4,"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Occurrence","depth":4,"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Filter Search Results","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Open in Window, Multiple Cursors","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Click to highlight","depth":4,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close","depth":4,"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":"[2026-05-11 10:41:16] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"ee9b9096-b1f1-4e2b-ac17-af0003eb00c0\",\"trace_id\":\"8904c61b-d5b4-44cc-91bf-68f0f73222c0\"}\n[2026-05-11 10:41:16] local.INFO: [EmailSchedule] STARTING batch process {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"ee9b9096-b1f1-4e2b-ac17-af0003eb00c0\",\"trace_id\":\"8904c61b-d5b4-44cc-91bf-68f0f73222c0\"}\n[2026-05-11 10:41:17] local.INFO: [EmailSchedule] FINISHED batch process {\"host\":\"docker_lamp_1\",\"processed\":0} {\"correlation_id\":\"ee9b9096-b1f1-4e2b-ac17-af0003eb00c0\",\"trace_id\":\"8904c61b-d5b4-44cc-91bf-68f0f73222c0\"}\n[2026-05-11 10:41:17] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":60.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"ee9b9096-b1f1-4e2b-ac17-af0003eb00c0\",\"trace_id\":\"8904c61b-d5b4-44cc-91bf-68f0f73222c0\"}\n[2026-05-11 10:41:21] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"crm:sync-hubspot-objects\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"13c8d2de-a598-41db-b4ca-9799a4bee946\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:21] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"crm:sync-hubspot-objects\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":60.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"13c8d2de-a598-41db-b4ca-9799a4bee946\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:21] local.INFO: [SyncHubspotObjects] Starting sync {\"team\":\"b2b115eb-93ce-4d1b-929c-173757df8fba\",\"usage\":24894088,\"real_usage\":65011712,\"pid\":62058} {\"correlation_id\":\"81433689-3fcd-4128-8b21-068ce5ee75be\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:21] local.WARNING: [HubSpot] Account not connected for user {\"userId\":\"33e34a7a-1c02-4f04-87ac-22c3a385e6e3\",\"account\":{\"Jiminny\\\\Models\\\\SocialAccount\":{\"id\":306,\"sociable_id\":109,\"provider_user_id\":\"11348452\",\"expires\":1701077403,\"refresh_token_expires\":null,\"provider\":\"hubspot\",\"state\":\"full-refresh\",\"auth_scope\":null,\"retry_after\":null,\"created_at\":\"2020-09-01 16:59:04\",\"updated_at\":\"2023-11-27 09:30:03\"}}} {\"correlation_id\":\"81433689-3fcd-4128-8b21-068ce5ee75be\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:21] local.INFO: [CrmOwnerResolver] Integration owner is not connected, attempting team members {\"crm_provider\":\"hubspot\",\"crm_owner\":109,\"team_id\":29} {\"correlation_id\":\"81433689-3fcd-4128-8b21-068ce5ee75be\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:21] local.INFO: [CrmOwnerResolver] No team members found with active crm connection {\"crm_provider\":\"hubspot\",\"team_id\":29} {\"correlation_id\":\"81433689-3fcd-4128-8b21-068ce5ee75be\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:21] local.INFO: [CrmOwnerResolver] No team member found with active crm connection {\"crm_provider\":\"hubspot\",\"team_id\":29} {\"correlation_id\":\"81433689-3fcd-4128-8b21-068ce5ee75be\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:21] local.INFO: [SyncHubspotObjects] Sync finished {\"team\":\"b2b115eb-93ce-4d1b-929c-173757df8fba\",\"provider\":\"hubspot\",\"status\":\"disconnected\",\"duration_ms\":35.85,\"usage\":24956128,\"real_usage\":65011712,\"pid\":62058,\"reason\":\"Your HubSpot account has become disconnected. Please login to Jiminny to reconnect.\"} {\"correlation_id\":\"81433689-3fcd-4128-8b21-068ce5ee75be\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:21] local.INFO: [SyncHubspotObjects] Starting sync {\"team\":\"abae74b8-bfa8-4383-9a7f-89f4bf2bdbb4\",\"usage\":24914344,\"real_usage\":65011712,\"pid\":62058} {\"correlation_id\":\"d2cb4aff-14c0-474f-89f2-f5c69dafe934\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:21] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"d2cb4aff-14c0-474f-89f2-f5c69dafe934\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:21] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"d2cb4aff-14c0-474f-89f2-f5c69dafe934\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:21] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d2cb4aff-14c0-474f-89f2-f5c69dafe934\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:21] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"d2cb4aff-14c0-474f-89f2-f5c69dafe934\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:21] local.INFO: [HubSpot] Syncing opportunities using strategy: lastModified {\"team\":2} {\"correlation_id\":\"d2cb4aff-14c0-474f-89f2-f5c69dafe934\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:21] local.INFO: [Hubspot] Pagination completed {\"team_id\":2,\"endpoint\":\"https://api.hubapi.com/crm/v3/objects/deals/search\",\"total_requests\":1,\"total_records_fetched\":0,\"total_elapsed_seconds\":0.26,\"average_seconds_per_request\":0.26} {\"correlation_id\":\"d2cb4aff-14c0-474f-89f2-f5c69dafe934\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:21] local.INFO: [HubSpot] Synced opportunities {\"team\":2,\"strategies\":\"lastModified\",\"sync_count\":0,\"total\":0,\"last_synced_id\":null,\"duration_ms\":288.63} {\"correlation_id\":\"d2cb4aff-14c0-474f-89f2-f5c69dafe934\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:21] local.INFO: [SyncHubspotObjects] Sync finished {\"team\":\"abae74b8-bfa8-4383-9a7f-89f4bf2bdbb4\",\"provider\":\"hubspot\",\"status\":\"completed\",\"duration_ms\":324.99,\"usage\":24978696,\"real_usage\":65011712,\"pid\":62058} {\"correlation_id\":\"d2cb4aff-14c0-474f-89f2-f5c69dafe934\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:22] local.INFO: [SyncHubspotObjects] Starting sync {\"team\":\"c6b9d6b0-b48d-4832-a68c-a57d60651888\",\"usage\":24956624,\"real_usage\":65011712,\"pid\":62058} {\"correlation_id\":\"955d9400-2f7f-4170-be31-43bafa1ef3bd\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:22] local.WARNING: [HubSpot] Account not connected for user {\"userId\":\"71e3aac5-fb66-47c5-a236-2d051ae3e319\",\"account\":null} {\"correlation_id\":\"955d9400-2f7f-4170-be31-43bafa1ef3bd\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:22] local.INFO: [CrmOwnerResolver] Integration owner is not connected, attempting team members {\"crm_provider\":\"hubspot\",\"crm_owner\":256,\"team_id\":49} {\"correlation_id\":\"955d9400-2f7f-4170-be31-43bafa1ef3bd\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:22] local.INFO: [CrmOwnerResolver] No team members found with active crm connection {\"crm_provider\":\"hubspot\",\"team_id\":49} {\"correlation_id\":\"955d9400-2f7f-4170-be31-43bafa1ef3bd\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:22] local.INFO: [CrmOwnerResolver] No team member found with active crm connection {\"crm_provider\":\"hubspot\",\"team_id\":49} {\"correlation_id\":\"955d9400-2f7f-4170-be31-43bafa1ef3bd\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:22] local.INFO: [SyncHubspotObjects] Sync finished {\"team\":\"c6b9d6b0-b48d-4832-a68c-a57d60651888\",\"provider\":\"hubspot\",\"status\":\"disconnected\",\"duration_ms\":17.51,\"usage\":24941048,\"real_usage\":65011712,\"pid\":62058,\"reason\":\"Social account for HubSpot cannot be found. Please login to Jiminny to connect.\"} {\"correlation_id\":\"955d9400-2f7f-4170-be31-43bafa1ef3bd\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:22] local.INFO: [SyncHubspotObjects] Starting sync {\"team\":\"b2d49a54-b645-4637-a7ae-a86cfce6e8e4\",\"usage\":24901688,\"real_usage\":65011712,\"pid\":62058} {\"correlation_id\":\"6d0b5717-9160-4c2f-ad12-56e42a2c544a\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:22] local.WARNING: [HubSpot] Account not connected for user {\"userId\":\"2ac0447f-3c8c-4ce0-baeb-b63ddb76fa9b\",\"account\":null} {\"correlation_id\":\"6d0b5717-9160-4c2f-ad12-56e42a2c544a\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:22] local.INFO: [CrmOwnerResolver] Integration owner is not connected, attempting team members {\"crm_provider\":\"hubspot\",\"crm_owner\":130,\"team_id\":42} {\"correlation_id\":\"6d0b5717-9160-4c2f-ad12-56e42a2c544a\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:22] local.INFO: [CrmOwnerResolver] No team members found with active crm connection {\"crm_provider\":\"hubspot\",\"team_id\":42} {\"correlation_id\":\"6d0b5717-9160-4c2f-ad12-56e42a2c544a\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:22] local.INFO: [CrmOwnerResolver] No team member found with active crm connection {\"crm_provider\":\"hubspot\",\"team_id\":42} {\"correlation_id\":\"6d0b5717-9160-4c2f-ad12-56e42a2c544a\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:22] local.INFO: [SyncHubspotObjects] Sync finished {\"team\":\"b2d49a54-b645-4637-a7ae-a86cfce6e8e4\",\"provider\":\"hubspot\",\"status\":\"disconnected\",\"duration_ms\":10.99,\"usage\":24944632,\"real_usage\":65011712,\"pid\":62058,\"reason\":\"Social account for HubSpot cannot be found. Please login to Jiminny to connect.\"} {\"correlation_id\":\"6d0b5717-9160-4c2f-ad12-56e42a2c544a\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:39] local.INFO: [HubSpot Journal Polling] No data {\"correlation_id\":\"1f8f11e6-0cf5-49dd-b8d7-56f51ec27f8e\",\"trace_id\":\"0f08076e-448d-487e-99a0-e73dae5c0ad4\"}\n[2026-05-11 10:41:39] local.WARNING: [HubSpot Journal Polling] Maximum empty results reached, stopping {\"empty_results\":5,\"max_empty_results\":5} {\"correlation_id\":\"1f8f11e6-0cf5-49dd-b8d7-56f51ec27f8e\",\"trace_id\":\"0f08076e-448d-487e-99a0-e73dae5c0ad4\"}\n[2026-05-11 10:41:39] local.WARNING: [HubSpot Journal Polling] Maximum empty results reached, stopping {\"empty_results\":5,\"max_empty_results\":5} {\"correlation_id\":\"1f8f11e6-0cf5-49dd-b8d7-56f51ec27f8e\",\"trace_id\":\"0f08076e-448d-487e-99a0-e73dae5c0ad4\"}\n[2026-05-11 10:41:39] local.INFO: [HubSpot Journal Polling] Service ending {\"runtime_seconds\":57,\"total_cycles\":5,\"files_downloaded\":0,\"empty_files\":0,\"other_portal_skipped\":0,\"total_events\":0,\"events_per_file\":0,\"avg_api_ms\":296.8,\"avg_download_ms\":0.0,\"avg_transform_ms\":0.0,\"avg_process_ms\":0.0,\"peak_memory_mb\":99.73} {\"correlation_id\":\"1f8f11e6-0cf5-49dd-b8d7-56f51ec27f8e\",\"trace_id\":\"0f08076e-448d-487e-99a0-e73dae5c0ad4\"}\n[2026-05-11 10:41:39] local.INFO: [HubSpot Journal Polling] Saved offset to database on cleanup {\"offset\":\"019e15a9-9ea0-7da7-87bc-82592e3ccf0d\"} {\"correlation_id\":\"1f8f11e6-0cf5-49dd-b8d7-56f51ec27f8e\",\"trace_id\":\"0f08076e-448d-487e-99a0-e73dae5c0ad4\"}\n[2026-05-11 10:41:39] local.INFO: [HubSpot Journal Polling] Released polling lock {\"correlation_id\":\"1f8f11e6-0cf5-49dd-b8d7-56f51ec27f8e\",\"trace_id\":\"0f08076e-448d-487e-99a0-e73dae5c0ad4\"}\n[2026-05-11 10:42:07] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"1aa66921-0a71-4d61-8cd7-0663242dca44\",\"trace_id\":\"f6b1d739-39b6-4043-a81e-a1af2c76d401\"}\n[2026-05-11 10:42:07] local.INFO: [ScheduleBotCommand] Number of activities to be captured: 0 {\"correlation_id\":\"1aa66921-0a71-4d61-8cd7-0663242dca44\",\"trace_id\":\"f6b1d739-39b6-4043-a81e-a1af2c76d401\"}\n[2026-05-11 10:42:07] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":60.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"1aa66921-0a71-4d61-8cd7-0663242dca44\",\"trace_id\":\"f6b1d739-39b6-4043-a81e-a1af2c76d401\"}\n[2026-05-11 10:42:10] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"cc420ba5-39bf-4183-95ae-48962a9c124b\",\"trace_id\":\"8daf6081-5c8a-4fae-98d8-e317d8580bc3\"}\n[2026-05-11 10:42:10] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":60.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"cc420ba5-39bf-4183-95ae-48962a9c124b\",\"trace_id\":\"8daf6081-5c8a-4fae-98d8-e317d8580bc3\"}\n[2026-05-11 10:42:13] local.NOTICE: Monitoring start {\"correlation_id\":\"50696062-587c-4c43-8075-7229916ea46f\",\"trace_id\":\"b58f754c-906f-413b-80e6-f89189dbe078\"}\n[2026-05-11 10:42:13] local.NOTICE: Monitoring end {\"correlation_id\":\"50696062-587c-4c43-8075-7229916ea46f\",\"trace_id\":\"b58f754c-906f-413b-80e6-f89189dbe078\"}\n[2026-05-11 10:42:17] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"f70656cd-6de3-4f8b-bb9d-74a941c552c4\",\"trace_id\":\"c780d8e3-2abd-41a3-a9dc-7c2be6aa22d4\"}\n[2026-05-11 10:42:17] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":60.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"f70656cd-6de3-4f8b-bb9d-74a941c552c4\",\"trace_id\":\"c780d8e3-2abd-41a3-a9dc-7c2be6aa22d4\"}\n[2026-05-11 10:42:20] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"b2e73601-1777-4ddb-a152-fe85f4c89afc\",\"trace_id\":\"8bac5054-216a-4605-b13b-9efab8ab5341\"}\n[2026-05-11 10:42:20] local.INFO: [EmailSchedule] STARTING batch process {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"b2e73601-1777-4ddb-a152-fe85f4c89afc\",\"trace_id\":\"8bac5054-216a-4605-b13b-9efab8ab5341\"}\n[2026-05-11 10:42:20] local.INFO: [EmailSchedule] FINISHED batch process {\"host\":\"docker_lamp_1\",\"processed\":0} {\"correlation_id\":\"b2e73601-1777-4ddb-a152-fe85f4c89afc\",\"trace_id\":\"8bac5054-216a-4605-b13b-9efab8ab5341\"}\n[2026-05-11 10:42:20] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":60.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"b2e73601-1777-4ddb-a152-fe85f4c89afc\",\"trace_id\":\"8bac5054-216a-4605-b13b-9efab8ab5341\"}\n[2026-05-11 10:42:31] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"conference:monitor:count\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"175d1973-e4ab-47f7-bb25-14bc3f0a1cce\",\"trace_id\":\"723f475a-f2fc-4235-8163-c2a8ed5777e6\"}\n[2026-05-11 10:42:31] local.INFO: Running conference:monitor:count command for activities in (2026-05-11 10:40:00, 2026-05-11 10:42:00] {\"correlation_id\":\"175d1973-e4ab-47f7-bb25-14bc3f0a1cce\",\"trace_id\":\"723f475a-f2fc-4235-8163-c2a8ed5777e6\"}\n[2026-05-11 10:42:31] local.INFO: [conference:monitor:count] No activities found in (2026-05-11 10:40:00, 2026-05-11 10:42:00] {\"correlation_id\":\"175d1973-e4ab-47f7-bb25-14bc3f0a1cce\",\"trace_id\":\"723f475a-f2fc-4235-8163-c2a8ed5777e6\"}\n[2026-05-11 10:42:31] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"conference:monitor:count\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":60.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"175d1973-e4ab-47f7-bb25-14bc3f0a1cce\",\"trace_id\":\"723f475a-f2fc-4235-8163-c2a8ed5777e6\"}\n[2026-05-11 10:42:36] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:create\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"4243f43b-9d70-4dd0-9c98-3122eb9445ed\",\"trace_id\":\"0c811e1d-ed7e-4aa7-ba81-a14a0cf4b306\"}\n[2026-05-11 10:42:36] local.INFO: [EmailSchedule] STARTING batch create {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"4243f43b-9d70-4dd0-9c98-3122eb9445ed\",\"trace_id\":\"0c811e1d-ed7e-4aa7-ba81-a14a0cf4b306\"}\n[2026-05-11 10:42:36] local.INFO: [EmailSchedule] FINISHED batch create {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"4243f43b-9d70-4dd0-9c98-3122eb9445ed\",\"trace_id\":\"0c811e1d-ed7e-4aa7-ba81-a14a0cf4b306\"}\n[2026-05-11 10:42:36] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:create\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":60.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"4243f43b-9d70-4dd0-9c98-3122eb9445ed\",\"trace_id\":\"0c811e1d-ed7e-4aa7-ba81-a14a0cf4b306\"}\n[2026-05-11 10:42:39] local.INFO: [Jiminny\\Jobs\\Mailbox\\CreateBatches] processed 2 inboxes and created 0 batches {\"userId\":null,\"batchSize\":30,\"maxBatches\":1000} {\"correlation_id\":\"2328b74c-d9ea-43ef-95db-20ea7c6ec9f5\",\"trace_id\":\"0c811e1d-ed7e-4aa7-ba81-a14a0cf4b306\"}\n[2026-05-11 10:42:54] local.INFO: [HubSpot Webhook] Signature validation started {\"method\":\"POST\",\"uri\":\"/webhook/conference/hubspot/events\",\"has_v3_signature\":true,\"has_v1_signature\":true} {\"correlation_id\":\"35391537-e747-4658-94f7-9d3d8c65eab6\",\"trace_id\":\"163e44b3-9f8e-4957-b04d-4937c33d30e6\"}\n[2026-05-11 10:42:54] local.INFO: [HubSpot Webhook] Event received {\"payload\":[{\"eventId\":1531490889,\"subscriptionId\":5683273,\"portalId\":4392066,\"appId\":38482,\"occurredAt\":1778478181931,\"subscriptionType\":\"company.associationChange\",\"attemptNumber\":8,\"changeSource\":\"USER\",\"associationType\":\"COMPANY_TO_DEAL\",\"fromObjectId\":13266161465,\"toObjectId\":60142058500,\"associationRemoved\":false,\"isPrimaryAssociation\":false}],\"event_count\":1} {\"correlation_id\":\"35391537-e747-4658-94f7-9d3d8c65eab6\",\"trace_id\":\"163e44b3-9f8e-4957-b04d-4937c33d30e6\"}\n[2026-05-11 10:42:54] local.INFO: [HubSpot Webhook Job] Processing webhook events {\"event_count\":1,\"local_count\":1,\"forward_count\":1} {\"correlation_id\":\"bb5ee15c-0826-40ef-a4c4-83761c307c7b\",\"trace_id\":\"163e44b3-9f8e-4957-b04d-4937c33d30e6\"}\n[2026-05-11 10:42:55] local.INFO: [Webhook Forwarder] Forwarding to instance {\"url\":\"https://uranus.staging.jiminny.com/internal/webhook-receiver/hubspot\",\"route\":\"internal.webhook-receiver.hubspot\",\"event_count\":1} {\"correlation_id\":\"bb5ee15c-0826-40ef-a4c4-83761c307c7b\",\"trace_id\":\"163e44b3-9f8e-4957-b04d-4937c33d30e6\"}\n[2026-05-11 10:42:55] local.ERROR: [Webhook Forwarder] Failed to forward to instance {\"url\":\"https://uranus.staging.jiminny.com\",\"route\":\"internal.webhook-receiver.hubspot\",\"error\":\"Server error: `POST https://uranus.staging.jiminny.com/internal/webhook-receiver/hubspot` resulted in a `503 Service Temporarily Unavailable` response:\n<html>\n\n<head><title>503 Service Temporarily Unavailable</title></head>\n\n<body>\n\n<center><h1>503 Service Temporarily Una (truncated...)\n\",\"code\":503} {\"correlation_id\":\"bb5ee15c-0826-40ef-a4c4-83761c307c7b\",\"trace_id\":\"163e44b3-9f8e-4957-b04d-4937c33d30e6\"}\n[2026-05-11 10:42:55] local.INFO: [Webhook Forwarder] Forwarding to instance {\"url\":\"https://app.qai.jiminny.com/internal/webhook-receiver/hubspot\",\"route\":\"internal.webhook-receiver.hubspot\",\"event_count\":1} {\"correlation_id\":\"bb5ee15c-0826-40ef-a4c4-83761c307c7b\",\"trace_id\":\"163e44b3-9f8e-4957-b04d-4937c33d30e6\"}\n[2026-05-11 10:42:56] local.INFO: [Webhook Forwarder] Successfully forwarded to instance {\"url\":\"https://app.qai.jiminny.com/internal/webhook-receiver/hubspot\",\"status_code\":202,\"response\":{\"status\":\"accepted\",\"message\":\"Events queued for processing\",\"event_count\":1}} {\"correlation_id\":\"bb5ee15c-0826-40ef-a4c4-83761c307c7b\",\"trace_id\":\"163e44b3-9f8e-4957-b04d-4937c33d30e6\"}\n[2026-05-11 10:42:56] local.INFO: [BatchSyncCollector] Stored webhook in Redis {\"object_type\":\"company\",\"crm_provider_id\":\"13266161465\",\"event_type\":\"association_change\",\"configuration_id\":2,\"batch_key\":\"batch_sync_company:ids:2:all\",\"was_new\":true,\"current_size\":1} {\"correlation_id\":\"bb5ee15c-0826-40ef-a4c4-83761c307c7b\",\"trace_id\":\"163e44b3-9f8e-4957-b04d-4937c33d30e6\"}\n[2026-05-11 10:43:14] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"a2a4326d-0416-4ea7-8eac-370e0fee93fb\",\"trace_id\":\"49cf2405-4c7b-48ce-9aac-074f08037e94\"}\n[2026-05-11 10:43:14] local.INFO: [ScheduleBotCommand] Number of activities to be captured: 0 {\"correlation_id\":\"a2a4326d-0416-4ea7-8eac-370e0fee93fb\",\"trace_id\":\"49cf2405-4c7b-48ce-9aac-074f08037e94\"}\n[2026-05-11 10:43:14] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":60.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"a2a4326d-0416-4ea7-8eac-370e0fee93fb\",\"trace_id\":\"49cf2405-4c7b-48ce-9aac-074f08037e94\"}\n[2026-05-11 10:43:18] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":615092,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"e7be542b-ee51-44b8-a563-a2eac45b3413\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:18] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":615092} {\"correlation_id\":\"e7be542b-ee51-44b8-a563-a2eac45b3413\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:18] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":615092,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"e7be542b-ee51-44b8-a563-a2eac45b3413\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:18] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":615092,\"participants\":[{\"id\":1004102,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":1004103,\"user_id\":89,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"e7be542b-ee51-44b8-a563-a2eac45b3413\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:19] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"e7be542b-ee51-44b8-a563-a2eac45b3413\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:19] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"e7be542b-ee51-44b8-a563-a2eac45b3413\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:19] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"e7be542b-ee51-44b8-a563-a2eac45b3413\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:19] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"e7be542b-ee51-44b8-a563-a2eac45b3413\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:20] local.INFO: [Prospect match] Cache miss, calling the API {\"identifier_type\":\"email\",\"identifier\":\"nikolay.nikolov@jiminny.com\"} {\"correlation_id\":\"e7be542b-ee51-44b8-a563-a2eac45b3413\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:20] local.INFO: [Hubspot] Failed to fetch contact {\"email\":\"nikolay.nikolov@jiminny.com\",\"reason\":\"[404] Client error: `GET https://api.hubapi.com/crm/v3/objects/contacts/nikolay.nikolov%40jiminny.com?properties=email%2Cfirstname%2Clastname%2Ccountry%2Cphone%2Cmobilephone%2Cjobtitle%2Chubspot_owner_id%2Cassociatedcompanyid%2Cphoto&archived=0&idProperty=email` resulted in a `404 Not Found` response\"} {\"correlation_id\":\"e7be542b-ee51-44b8-a563-a2eac45b3413\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:20] local.INFO: [Prospect match] API returned empty result, caching the miss with empty prospect data {\"identifier_type\":\"email\",\"identifier\":\"nikolay.nikolov@jiminny.com\"} {\"correlation_id\":\"e7be542b-ee51-44b8-a563-a2eac45b3413\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:21] local.INFO: [Hubspot] Pagination completed {\"team_id\":2,\"endpoint\":\"https://api.hubapi.com/crm/v3/objects/contact/search\",\"total_requests\":1,\"total_records_fetched\":0,\"total_elapsed_seconds\":0.38,\"average_seconds_per_request\":0.38} {\"correlation_id\":\"e7be542b-ee51-44b8-a563-a2eac45b3413\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:22] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"ab7124d8-7931-41ef-8341-93a3305f65eb\",\"trace_id\":\"4564d24d-bf9e-486b-ac8e-0f69c9b286d8\"}\n[2026-05-11 10:43:22] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":60.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"ab7124d8-7931-41ef-8341-93a3305f65eb\",\"trace_id\":\"4564d24d-bf9e-486b-ac8e-0f69c9b286d8\"}\n[2026-05-11 10:43:22] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"nikolay.nikolov@jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"e7be542b-ee51-44b8-a563-a2eac45b3413\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:22] local.INFO: [Prospect match] Cache miss {\"identifier_type\":\"domain\",\"identifier\":\"jiminny.com\",\"crm\":\"hubspot\"} {\"correlation_id\":\"e7be542b-ee51-44b8-a563-a2eac45b3413\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:22] local.INFO: [Prospect match] Cache miss, calling the API {\"identifier_type\":\"domain\",\"identifier\":\"jiminny.com\"} {\"correlation_id\":\"e7be542b-ee51-44b8-a563-a2eac45b3413\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:23] local.INFO: [HubSpot] importAccount {\"crm_provider_id\":\"749766179\",\"config_id\":2} {\"correlation_id\":\"e7be542b-ee51-44b8-a563-a2eac45b3413\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:23] local.INFO: [HubSpot] CRM Search requested {\"request\":{\"filterGroups\":[{\"filters\":[{\"propertyName\":\"associations.company\",\"operator\":\"EQ\",\"value\":\"749766179\"},{\"propertyName\":\"dealstage\",\"operator\":\"NOT_IN\",\"values\":[\"closedwon\",\"4040964\",\"59247967\"]},{\"propertyName\":\"dealstage\",\"operator\":\"NOT_IN\",\"values\":[\"closedlost\",\"4040965\",\"59247968\"]}]}],\"sorts\":[{\"propertyName\":\"modifieddate\",\"direction\":\"DESCENDING\"}],\"properties\":[\"dealname\",\"amount\",\"hubspot_owner_id\",\"pipeline\",\"dealstage\",\"closedate\",\"deal_currency_code\"],\"limit\":200}} {\"correlation_id\":\"e7be542b-ee51-44b8-a563-a2eac45b3413\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:23] local.INFO: [Hubspot] Pagination completed {\"team_id\":2,\"endpoint\":\"https://api.hubapi.com/crm/v3/objects/deals/search\",\"total_requests\":1,\"total_records_fetched\":10,\"total_elapsed_seconds\":0.3,\"average_seconds_per_request\":0.3} {\"correlation_id\":\"e7be542b-ee51-44b8-a563-a2eac45b3413\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:24] local.INFO: [ EsUpdateProcessManager ] Finished updating entities in ES {\"worker\":\"\",\"peak_memory\":\"99.73 MB\",\"elapsed_seconds\":3.22,\"update_target\":\"activities\",\"should_iterate_again\":false} {\"correlation_id\":\"4ad05333-9afb-492e-9f0f-b2909ac45b32\",\"trace_id\":\"3d8feb24-b173-4158-b0a4-4cf33af85066\"}\n[2026-05-11 10:43:24] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":615092,\"team_id\":2,\"email\":\"nikolay.nikolov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"e7be542b-ee51-44b8-a563-a2eac45b3413\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:24] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":615092,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"e7be542b-ee51-44b8-a563-a2eac45b3413\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:24] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":615092} {\"correlation_id\":\"e7be542b-ee51-44b8-a563-a2eac45b3413\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:24] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":615092,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"e7be542b-ee51-44b8-a563-a2eac45b3413\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:24] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":615092,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"e7be542b-ee51-44b8-a563-a2eac45b3413\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:24] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":614436,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"a34067d1-038f-48bf-af25-c9e09374ce62\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:24] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":614436} {\"correlation_id\":\"a34067d1-038f-48bf-af25-c9e09374ce62\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:24] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":614436,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"a34067d1-038f-48bf-af25-c9e09374ce62\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:24] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":614436,\"participants\":[{\"id\":1002751,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":1002752,\"user_id\":89,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"a34067d1-038f-48bf-af25-c9e09374ce62\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:24] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"a34067d1-038f-48bf-af25-c9e09374ce62\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:24] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"a34067d1-038f-48bf-af25-c9e09374ce62\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:24] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"a34067d1-038f-48bf-af25-c9e09374ce62\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:24] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"a34067d1-038f-48bf-af25-c9e09374ce62\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:24] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"nikolay.nikolov@jiminny.com\"} {\"correlation_id\":\"a34067d1-038f-48bf-af25-c9e09374ce62\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:24] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"nikolay.nikolov@jiminny.com\"} {\"correlation_id\":\"a34067d1-038f-48bf-af25-c9e09374ce62\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:24] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"nikolay.nikolov@jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"a34067d1-038f-48bf-af25-c9e09374ce62\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:24] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":614436,\"team_id\":2,\"email\":\"nikolay.nikolov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"a34067d1-038f-48bf-af25-c9e09374ce62\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:24] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":614436,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"a34067d1-038f-48bf-af25-c9e09374ce62\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:24] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":614436} {\"correlation_id\":\"a34067d1-038f-48bf-af25-c9e09374ce62\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:24] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":614436,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"a34067d1-038f-48bf-af25-c9e09374ce62\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:24] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":614436,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"a34067d1-038f-48bf-af25-c9e09374ce62\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":614382,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"ee88af0b-20d3-4911-a378-bea7fc6dc3f6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":614382} {\"correlation_id\":\"ee88af0b-20d3-4911-a378-bea7fc6dc3f6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":614382,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"ee88af0b-20d3-4911-a378-bea7fc6dc3f6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":614382,\"participants\":[{\"id\":1002632,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":1002633,\"user_id\":89,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"ee88af0b-20d3-4911-a378-bea7fc6dc3f6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"ee88af0b-20d3-4911-a378-bea7fc6dc3f6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"ee88af0b-20d3-4911-a378-bea7fc6dc3f6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"ee88af0b-20d3-4911-a378-bea7fc6dc3f6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"ee88af0b-20d3-4911-a378-bea7fc6dc3f6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"nikolay.nikolov@jiminny.com\"} {\"correlation_id\":\"ee88af0b-20d3-4911-a378-bea7fc6dc3f6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"nikolay.nikolov@jiminny.com\"} {\"correlation_id\":\"ee88af0b-20d3-4911-a378-bea7fc6dc3f6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"nikolay.nikolov@jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"ee88af0b-20d3-4911-a378-bea7fc6dc3f6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":614382,\"team_id\":2,\"email\":\"nikolay.nikolov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"ee88af0b-20d3-4911-a378-bea7fc6dc3f6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":614382,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"ee88af0b-20d3-4911-a378-bea7fc6dc3f6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":614382} {\"correlation_id\":\"ee88af0b-20d3-4911-a378-bea7fc6dc3f6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":614382,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"ee88af0b-20d3-4911-a378-bea7fc6dc3f6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":614382,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"ee88af0b-20d3-4911-a378-bea7fc6dc3f6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":614381,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"535b99f0-d38e-4b9c-bf19-3751ce4e6a1d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":614381} {\"correlation_id\":\"535b99f0-d38e-4b9c-bf19-3751ce4e6a1d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":614381,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"535b99f0-d38e-4b9c-bf19-3751ce4e6a1d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":614381,\"participants\":[{\"id\":1002630,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":1002631,\"user_id\":89,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"535b99f0-d38e-4b9c-bf19-3751ce4e6a1d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"535b99f0-d38e-4b9c-bf19-3751ce4e6a1d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"535b99f0-d38e-4b9c-bf19-3751ce4e6a1d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"535b99f0-d38e-4b9c-bf19-3751ce4e6a1d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"535b99f0-d38e-4b9c-bf19-3751ce4e6a1d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"nikolay.nikolov@jiminny.com\"} {\"correlation_id\":\"535b99f0-d38e-4b9c-bf19-3751ce4e6a1d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"nikolay.nikolov@jiminny.com\"} {\"correlation_id\":\"535b99f0-d38e-4b9c-bf19-3751ce4e6a1d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"nikolay.nikolov@jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"535b99f0-d38e-4b9c-bf19-3751ce4e6a1d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":614381,\"team_id\":2,\"email\":\"nikolay.nikolov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"535b99f0-d38e-4b9c-bf19-3751ce4e6a1d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":614381,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"535b99f0-d38e-4b9c-bf19-3751ce4e6a1d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":614381} {\"correlation_id\":\"535b99f0-d38e-4b9c-bf19-3751ce4e6a1d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":614381,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"535b99f0-d38e-4b9c-bf19-3751ce4e6a1d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":614381,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"535b99f0-d38e-4b9c-bf19-3751ce4e6a1d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":614378,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":6167,\"account_id\":null,\"opportunity_id\":null,\"stage_id\":null}} {\"correlation_id\":\"f934b469-16df-4a4b-b5cc-bf918da87b87\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":614378} {\"correlation_id\":\"f934b469-16df-4a4b-b5cc-bf918da87b87\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":614378,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"f934b469-16df-4a4b-b5cc-bf918da87b87\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":614378,\"participants\":[{\"id\":1002623,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":1002624,\"user_id\":null,\"contact_id\":6167,\"lead_id\":null},{\"id\":1002625,\"user_id\":89,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"f934b469-16df-4a4b-b5cc-bf918da87b87\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"f934b469-16df-4a4b-b5cc-bf918da87b87\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"f934b469-16df-4a4b-b5cc-bf918da87b87\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"f934b469-16df-4a4b-b5cc-bf918da87b87\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"f934b469-16df-4a4b-b5cc-bf918da87b87\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"nikolay.nikolov@jiminny.com\"} {\"correlation_id\":\"f934b469-16df-4a4b-b5cc-bf918da87b87\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"nikolay.nikolov@jiminny.com\"} {\"correlation_id\":\"f934b469-16df-4a4b-b5cc-bf918da87b87\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"nikolay.nikolov@jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"f934b469-16df-4a4b-b5cc-bf918da87b87\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"nmalchev@gmail.com\"} {\"correlation_id\":\"f934b469-16df-4a4b-b5cc-bf918da87b87\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":614378,\"team_id\":2,\"email\":\"nikolay.nikolov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"f934b469-16df-4a4b-b5cc-bf918da87b87\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":614378,\"participants_processed\":3,\"exact_matches\":1,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"f934b469-16df-4a4b-b5cc-bf918da87b87\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":614378} {\"correlation_id\":\"f934b469-16df-4a4b-b5cc-bf918da87b87\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":614378,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"f934b469-16df-4a4b-b5cc-bf918da87b87\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":614378,\"remote_search\":true,\"lead_id\":null,\"contact_id\":6167,\"account_id\":null,\"opportunity_id\":null,\"stage_id\":null} {\"correlation_id\":\"f934b469-16df-4a4b-b5cc-bf918da87b87\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":613840,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36}} {\"correlation_id\":\"b440c986-63bd-445c-adfc-4d111801e63c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613840} {\"correlation_id\":\"b440c986-63bd-445c-adfc-4d111801e63c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613840,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"b440c986-63bd-445c-adfc-4d111801e63c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":613840,\"participants\":[{\"id\":1001764,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null},{\"id\":1001765,\"user_id\":261,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"b440c986-63bd-445c-adfc-4d111801e63c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"b440c986-63bd-445c-adfc-4d111801e63c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"b440c986-63bd-445c-adfc-4d111801e63c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"b440c986-63bd-445c-adfc-4d111801e63c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"b440c986-63bd-445c-adfc-4d111801e63c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: ProspectCache - Searching DB for opportunity by owner {\"account_id\":244,\"contact_id\":4487,\"owner_id\":261} {\"correlation_id\":\"b440c986-63bd-445c-adfc-4d111801e63c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: ProspectCache - Opportunity DB search results {\"account_id\":244,\"contact_id\":4487,\"opportunity_id\":299} {\"correlation_id\":\"b440c986-63bd-445c-adfc-4d111801e63c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"b440c986-63bd-445c-adfc-4d111801e63c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":613840,\"team_id\":2,\"email\":\"adelina.petrova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"b440c986-63bd-445c-adfc-4d111801e63c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":613840,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"b440c986-63bd-445c-adfc-4d111801e63c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613840} {\"correlation_id\":\"b440c986-63bd-445c-adfc-4d111801e63c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613840,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"b440c986-63bd-445c-adfc-4d111801e63c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":613840,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36} {\"correlation_id\":\"b440c986-63bd-445c-adfc-4d111801e63c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":613833,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36}} {\"correlation_id\":\"0360a22a-94dd-43cb-b57b-06bc159a05ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613833} {\"correlation_id\":\"0360a22a-94dd-43cb-b57b-06bc159a05ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613833,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"0360a22a-94dd-43cb-b57b-06bc159a05ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":613833,\"participants\":[{\"id\":1001750,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null},{\"id\":1001751,\"user_id\":261,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"0360a22a-94dd-43cb-b57b-06bc159a05ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"0360a22a-94dd-43cb-b57b-06bc159a05ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"0360a22a-94dd-43cb-b57b-06bc159a05ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"0360a22a-94dd-43cb-b57b-06bc159a05ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"0360a22a-94dd-43cb-b57b-06bc159a05ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"0360a22a-94dd-43cb-b57b-06bc159a05ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":613833,\"team_id\":2,\"email\":\"adelina.petrova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"0360a22a-94dd-43cb-b57b-06bc159a05ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":613833,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"0360a22a-94dd-43cb-b57b-06bc159a05ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613833} {\"correlation_id\":\"0360a22a-94dd-43cb-b57b-06bc159a05ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613833,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"0360a22a-94dd-43cb-b57b-06bc159a05ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":613833,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36} {\"correlation_id\":\"0360a22a-94dd-43cb-b57b-06bc159a05ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":613827,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36}} {\"correlation_id\":\"32fbe130-bf29-4e37-9c5a-bbe81f416a36\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613827} {\"correlation_id\":\"32fbe130-bf29-4e37-9c5a-bbe81f416a36\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613827,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"32fbe130-bf29-4e37-9c5a-bbe81f416a36\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":613827,\"participants\":[{\"id\":1001734,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null},{\"id\":1001735,\"user_id\":261,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"32fbe130-bf29-4e37-9c5a-bbe81f416a36\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"32fbe130-bf29-4e37-9c5a-bbe81f416a36\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"32fbe130-bf29-4e37-9c5a-bbe81f416a36\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"32fbe130-bf29-4e37-9c5a-bbe81f416a36\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"32fbe130-bf29-4e37-9c5a-bbe81f416a36\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"32fbe130-bf29-4e37-9c5a-bbe81f416a36\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":613827,\"team_id\":2,\"email\":\"adelina.petrova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"32fbe130-bf29-4e37-9c5a-bbe81f416a36\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":613827,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"32fbe130-bf29-4e37-9c5a-bbe81f416a36\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613827} {\"correlation_id\":\"32fbe130-bf29-4e37-9c5a-bbe81f416a36\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613827,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"32fbe130-bf29-4e37-9c5a-bbe81f416a36\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":613827,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36} {\"correlation_id\":\"32fbe130-bf29-4e37-9c5a-bbe81f416a36\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":613826,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36}} {\"correlation_id\":\"9b9716ff-ede9-4377-8ee8-b3c9a68b2dfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613826} {\"correlation_id\":\"9b9716ff-ede9-4377-8ee8-b3c9a68b2dfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613826,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"9b9716ff-ede9-4377-8ee8-b3c9a68b2dfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":613826,\"participants\":[{\"id\":1001732,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null},{\"id\":1001733,\"user_id\":261,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"9b9716ff-ede9-4377-8ee8-b3c9a68b2dfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"9b9716ff-ede9-4377-8ee8-b3c9a68b2dfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"9b9716ff-ede9-4377-8ee8-b3c9a68b2dfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"9b9716ff-ede9-4377-8ee8-b3c9a68b2dfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"9b9716ff-ede9-4377-8ee8-b3c9a68b2dfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"9b9716ff-ede9-4377-8ee8-b3c9a68b2dfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":613826,\"team_id\":2,\"email\":\"adelina.petrova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"9b9716ff-ede9-4377-8ee8-b3c9a68b2dfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":613826,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"9b9716ff-ede9-4377-8ee8-b3c9a68b2dfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613826} {\"correlation_id\":\"9b9716ff-ede9-4377-8ee8-b3c9a68b2dfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613826,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"9b9716ff-ede9-4377-8ee8-b3c9a68b2dfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":613826,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36} {\"correlation_id\":\"9b9716ff-ede9-4377-8ee8-b3c9a68b2dfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":613820,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36}} {\"correlation_id\":\"a131075c-6ae7-4912-92e1-adc33c291d1e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613820} {\"correlation_id\":\"a131075c-6ae7-4912-92e1-adc33c291d1e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613820,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"a131075c-6ae7-4912-92e1-adc33c291d1e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":613820,\"participants\":[{\"id\":1001721,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null},{\"id\":1001722,\"user_id\":261,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"a131075c-6ae7-4912-92e1-adc33c291d1e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"a131075c-6ae7-4912-92e1-adc33c291d1e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"a131075c-6ae7-4912-92e1-adc33c291d1e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"a131075c-6ae7-4912-92e1-adc33c291d1e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"a131075c-6ae7-4912-92e1-adc33c291d1e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"a131075c-6ae7-4912-92e1-adc33c291d1e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":613820,\"team_id\":2,\"email\":\"adelina.petrova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"a131075c-6ae7-4912-92e1-adc33c291d1e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":613820,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"a131075c-6ae7-4912-92e1-adc33c291d1e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613820} {\"correlation_id\":\"a131075c-6ae7-4912-92e1-adc33c291d1e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613820,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"a131075c-6ae7-4912-92e1-adc33c291d1e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":613820,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36} {\"correlation_id\":\"a131075c-6ae7-4912-92e1-adc33c291d1e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":613818,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36}} {\"correlation_id\":\"d36e09b8-5b15-40f2-8fa0-0beb3f0ebe69\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613818} {\"correlation_id\":\"d36e09b8-5b15-40f2-8fa0-0beb3f0ebe69\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613818,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"d36e09b8-5b15-40f2-8fa0-0beb3f0ebe69\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":613818,\"participants\":[{\"id\":1001717,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null},{\"id\":1001718,\"user_id\":261,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"d36e09b8-5b15-40f2-8fa0-0beb3f0ebe69\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"d36e09b8-5b15-40f2-8fa0-0beb3f0ebe69\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"d36e09b8-5b15-40f2-8fa0-0beb3f0ebe69\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d36e09b8-5b15-40f2-8fa0-0beb3f0ebe69\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"d36e09b8-5b15-40f2-8fa0-0beb3f0ebe69\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"d36e09b8-5b15-40f2-8fa0-0beb3f0ebe69\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":613818,\"team_id\":2,\"email\":\"adelina.petrova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"d36e09b8-5b15-40f2-8fa0-0beb3f0ebe69\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":613818,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"d36e09b8-5b15-40f2-8fa0-0beb3f0ebe69\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613818} {\"correlation_id\":\"d36e09b8-5b15-40f2-8fa0-0beb3f0ebe69\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613818,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"d36e09b8-5b15-40f2-8fa0-0beb3f0ebe69\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":613818,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36} {\"correlation_id\":\"d36e09b8-5b15-40f2-8fa0-0beb3f0ebe69\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":613812,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36}} {\"correlation_id\":\"9e5ea20b-def9-4ace-8a73-9d08a04cc6d9\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613812} {\"correlation_id\":\"9e5ea20b-def9-4ace-8a73-9d08a04cc6d9\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613812,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"9e5ea20b-def9-4ace-8a73-9d08a04cc6d9\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":613812,\"participants\":[{\"id\":1001705,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null},{\"id\":1001706,\"user_id\":261,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"9e5ea20b-def9-4ace-8a73-9d08a04cc6d9\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"9e5ea20b-def9-4ace-8a73-9d08a04cc6d9\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"9e5ea20b-def9-4ace-8a73-9d08a04cc6d9\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"9e5ea20b-def9-4ace-8a73-9d08a04cc6d9\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"9e5ea20b-def9-4ace-8a73-9d08a04cc6d9\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"9e5ea20b-def9-4ace-8a73-9d08a04cc6d9\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":613812,\"team_id\":2,\"email\":\"adelina.petrova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"9e5ea20b-def9-4ace-8a73-9d08a04cc6d9\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":613812,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"9e5ea20b-def9-4ace-8a73-9d08a04cc6d9\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613812} {\"correlation_id\":\"9e5ea20b-def9-4ace-8a73-9d08a04cc6d9\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613812,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"9e5ea20b-def9-4ace-8a73-9d08a04cc6d9\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":613812,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36} {\"correlation_id\":\"9e5ea20b-def9-4ace-8a73-9d08a04cc6d9\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":613807,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4484,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"be53e876-a319-40dc-a89b-0728d9a47c6e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613807} {\"correlation_id\":\"be53e876-a319-40dc-a89b-0728d9a47c6e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613807,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"be53e876-a319-40dc-a89b-0728d9a47c6e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":613807,\"participants\":[{\"id\":1001690,\"user_id\":253,\"contact_id\":null,\"lead_id\":null},{\"id\":1001691,\"user_id\":null,\"contact_id\":4484,\"lead_id\":null}]} {\"correlation_id\":\"be53e876-a319-40dc-a89b-0728d9a47c6e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"be53e876-a319-40dc-a89b-0728d9a47c6e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"be53e876-a319-40dc-a89b-0728d9a47c6e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"be53e876-a319-40dc-a89b-0728d9a47c6e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"be53e876-a319-40dc-a89b-0728d9a47c6e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":613807,\"team_id\":2,\"email\":\"preslava.ivanova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"be53e876-a319-40dc-a89b-0728d9a47c6e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: ProspectCache - Searching DB for opportunity by owner {\"account_id\":243,\"contact_id\":4484,\"owner_id\":253} {\"correlation_id\":\"be53e876-a319-40dc-a89b-0728d9a47c6e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: ProspectCache - Fallback DB opportunity search {\"account_id\":243,\"contact_id\":4484} {\"correlation_id\":\"be53e876-a319-40dc-a89b-0728d9a47c6e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: ProspectCache - Opportunity DB search results {\"account_id\":243,\"contact_id\":4484,\"opportunity_id\":276} {\"correlation_id\":\"be53e876-a319-40dc-a89b-0728d9a47c6e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"tsvetomir.banovski@gmail.com\"} {\"correlation_id\":\"be53e876-a319-40dc-a89b-0728d9a47c6e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":613807,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"be53e876-a319-40dc-a89b-0728d9a47c6e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613807} {\"correlation_id\":\"be53e876-a319-40dc-a89b-0728d9a47c6e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613807,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"be53e876-a319-40dc-a89b-0728d9a47c6e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":613807,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4484,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"be53e876-a319-40dc-a89b-0728d9a47c6e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":613806,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":350,\"stage_id\":34}} {\"correlation_id\":\"94c53200-4779-41ab-82ea-571c1bcdd8b7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613806} {\"correlation_id\":\"94c53200-4779-41ab-82ea-571c1bcdd8b7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613806,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"94c53200-4779-41ab-82ea-571c1bcdd8b7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":613806,\"participants\":[{\"id\":1001688,\"user_id\":253,\"contact_id\":null,\"lead_id\":null},{\"id\":1001689,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null}]} {\"correlation_id\":\"94c53200-4779-41ab-82ea-571c1bcdd8b7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"94c53200-4779-41ab-82ea-571c1bcdd8b7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"94c53200-4779-41ab-82ea-571c1bcdd8b7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"94c53200-4779-41ab-82ea-571c1bcdd8b7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"94c53200-4779-41ab-82ea-571c1bcdd8b7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":613806,\"team_id\":2,\"email\":\"preslava.ivanova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"94c53200-4779-41ab-82ea-571c1bcdd8b7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: ProspectCache - Searching DB for opportunity by owner {\"account_id\":244,\"contact_id\":4487,\"owner_id\":253} {\"correlation_id\":\"94c53200-4779-41ab-82ea-571c1bcdd8b7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: ProspectCache - Fallback DB opportunity search {\"account_id\":244,\"contact_id\":4487} {\"correlation_id\":\"94c53200-4779-41ab-82ea-571c1bcdd8b7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: ProspectCache - Opportunity DB search results {\"account_id\":244,\"contact_id\":4487,\"opportunity_id\":350} {\"correlation_id\":\"94c53200-4779-41ab-82ea-571c1bcdd8b7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"94c53200-4779-41ab-82ea-571c1bcdd8b7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":613806,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"94c53200-4779-41ab-82ea-571c1bcdd8b7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613806} {\"correlation_id\":\"94c53200-4779-41ab-82ea-571c1bcdd8b7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613806,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"94c53200-4779-41ab-82ea-571c1bcdd8b7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":613806,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":350,\"stage_id\":34} {\"correlation_id\":\"94c53200-4779-41ab-82ea-571c1bcdd8b7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":613805,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":350,\"stage_id\":34}} {\"correlation_id\":\"d1a190ef-9b03-49ff-ad47-50afa53ef345\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613805} {\"correlation_id\":\"d1a190ef-9b03-49ff-ad47-50afa53ef345\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613805,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"d1a190ef-9b03-49ff-ad47-50afa53ef345\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":613805,\"participants\":[{\"id\":1001686,\"user_id\":253,\"contact_id\":null,\"lead_id\":null},{\"id\":1001687,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null}]} {\"correlation_id\":\"d1a190ef-9b03-49ff-ad47-50afa53ef345\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"d1a190ef-9b03-49ff-ad47-50afa53ef345\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"d1a190ef-9b03-49ff-ad47-50afa53ef345\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d1a190ef-9b03-49ff-ad47-50afa53ef345\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"d1a190ef-9b03-49ff-ad47-50afa53ef345\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":613805,\"team_id\":2,\"email\":\"preslava.ivanova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"d1a190ef-9b03-49ff-ad47-50afa53ef345\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"d1a190ef-9b03-49ff-ad47-50afa53ef345\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":613805,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"d1a190ef-9b03-49ff-ad47-50afa53ef345\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613805} {\"correlation_id\":\"d1a190ef-9b03-49ff-ad47-50afa53ef345\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613805,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"d1a190ef-9b03-49ff-ad47-50afa53ef345\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":613805,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":350,\"stage_id\":34} {\"correlation_id\":\"d1a190ef-9b03-49ff-ad47-50afa53ef345\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":613698,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36}} {\"correlation_id\":\"2fa5fec1-54fb-40cc-93d4-145712a9d2fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613698} {\"correlation_id\":\"2fa5fec1-54fb-40cc-93d4-145712a9d2fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613698,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"2fa5fec1-54fb-40cc-93d4-145712a9d2fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":613698,\"participants\":[{\"id\":1001667,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null},{\"id\":1001668,\"user_id\":261,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"2fa5fec1-54fb-40cc-93d4-145712a9d2fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"2fa5fec1-54fb-40cc-93d4-145712a9d2fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"2fa5fec1-54fb-40cc-93d4-145712a9d2fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"2fa5fec1-54fb-40cc-93d4-145712a9d2fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"2fa5fec1-54fb-40cc-93d4-145712a9d2fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"2fa5fec1-54fb-40cc-93d4-145712a9d2fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":613698,\"team_id\":2,\"email\":\"adelina.petrova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"2fa5fec1-54fb-40cc-93d4-145712a9d2fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":613698,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"2fa5fec1-54fb-40cc-93d4-145712a9d2fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613698} {\"correlation_id\":\"2fa5fec1-54fb-40cc-93d4-145712a9d2fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613698,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"2fa5fec1-54fb-40cc-93d4-145712a9d2fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":613698,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36} {\"correlation_id\":\"2fa5fec1-54fb-40cc-93d4-145712a9d2fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":613697,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36}} {\"correlation_id\":\"4da249c2-1056-47f9-869a-53c5fda5b9ef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613697} {\"correlation_id\":\"4da249c2-1056-47f9-869a-53c5fda5b9ef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613697,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"4da249c2-1056-47f9-869a-53c5fda5b9ef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":613697,\"participants\":[{\"id\":1001665,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null},{\"id\":1001666,\"user_id\":261,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"4da249c2-1056-47f9-869a-53c5fda5b9ef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"4da249c2-1056-47f9-869a-53c5fda5b9ef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"4da249c2-1056-47f9-869a-53c5fda5b9ef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4da249c2-1056-47f9-869a-53c5fda5b9ef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"4da249c2-1056-47f9-869a-53c5fda5b9ef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"4da249c2-1056-47f9-869a-53c5fda5b9ef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":613697,\"team_id\":2,\"email\":\"adelina.petrova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"4da249c2-1056-47f9-869a-53c5fda5b9ef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":613697,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"4da249c2-1056-47f9-869a-53c5fda5b9ef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613697} {\"correlation_id\":\"4da249c2-1056-47f9-869a-53c5fda5b9ef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613697,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"4da249c2-1056-47f9-869a-53c5fda5b9ef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":613697,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36} {\"correlation_id\":\"4da249c2-1056-47f9-869a-53c5fda5b9ef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":613696,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36}} {\"correlation_id\":\"8224abca-8e96-4a39-8baa-b4bc4f746e1b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613696} {\"correlation_id\":\"8224abca-8e96-4a39-8baa-b4bc4f746e1b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613696,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"8224abca-8e96-4a39-8baa-b4bc4f746e1b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":613696,\"participants\":[{\"id\":1001663,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null},{\"id\":1001664,\"user_id\":261,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"8224abca-8e96-4a39-8baa-b4bc4f746e1b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"8224abca-8e96-4a39-8baa-b4bc4f746e1b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"8224abca-8e96-4a39-8baa-b4bc4f746e1b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"8224abca-8e96-4a39-8baa-b4bc4f746e1b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"8224abca-8e96-4a39-8baa-b4bc4f746e1b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"8224abca-8e96-4a39-8baa-b4bc4f746e1b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":613696,\"team_id\":2,\"email\":\"adelina.petrova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"8224abca-8e96-4a39-8baa-b4bc4f746e1b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":613696,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"8224abca-8e96-4a39-8baa-b4bc4f746e1b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613696} {\"correlation_id\":\"8224abca-8e96-4a39-8baa-b4bc4f746e1b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613696,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"8224abca-8e96-4a39-8baa-b4bc4f746e1b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":613696,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36} {\"correlation_id\":\"8224abca-8e96-4a39-8baa-b4bc4f746e1b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":613695,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36}} {\"correlation_id\":\"e6c53892-ee7f-42df-ae80-06e4cab7eeea\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613695} {\"correlation_id\":\"e6c53892-ee7f-42df-ae80-06e4cab7eeea\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613695,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"e6c53892-ee7f-42df-ae80-06e4cab7eeea\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":613695,\"participants\":[{\"id\":1001661,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null},{\"id\":1001662,\"user_id\":261,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"e6c53892-ee7f-42df-ae80-06e4cab7eeea\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"e6c53892-ee7f-42df-ae80-06e4cab7eeea\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"e6c53892-ee7f-42df-ae80-06e4cab7eeea\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"e6c53892-ee7f-42df-ae80-06e4cab7eeea\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"e6c53892-ee7f-42df-ae80-06e4cab7eeea\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"e6c53892-ee7f-42df-ae80-06e4cab7eeea\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":613695,\"team_id\":2,\"email\":\"adelina.petrova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"e6c53892-ee7f-42df-ae80-06e4cab7eeea\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":613695,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"e6c53892-ee7f-42df-ae80-06e4cab7eeea\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613695} {\"correlation_id\":\"e6c53892-ee7f-42df-ae80-06e4cab7eeea\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613695,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"e6c53892-ee7f-42df-ae80-06e4cab7eeea\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":613695,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36} {\"correlation_id\":\"e6c53892-ee7f-42df-ae80-06e4cab7eeea\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.NOTICE: Monitoring start {\"correlation_id\":\"2766b9f0-0c60-4d94-9b69-4f2dc977fe59\",\"trace_id\":\"0810b004-42ab-49a4-b3bf-4fbda4a819fe\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":613694,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36}} {\"correlation_id\":\"34f7f8f5-b1b3-4176-bc4a-f96265db81c5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613694} {\"correlation_id\":\"34f7f8f5-b1b3-4176-bc4a-f96265db81c5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613694,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"34f7f8f5-b1b3-4176-bc4a-f96265db81c5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.NOTICE: Monitoring end {\"correlation_id\":\"2766b9f0-0c60-4d94-9b69-4f2dc977fe59\",\"trace_id\":\"0810b004-42ab-49a4-b3bf-4fbda4a819fe\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":613694,\"participants\":[{\"id\":1001659,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null},{\"id\":1001660,\"user_id\":261,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"34f7f8f5-b1b3-4176-bc4a-f96265db81c5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"34f7f8f5-b1b3-4176-bc4a-f96265db81c5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"34f7f8f5-b1b3-4176-bc4a-f96265db81c5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"34f7f8f5-b1b3-4176-bc4a-f96265db81c5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"34f7f8f5-b1b3-4176-bc4a-f96265db81c5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"34f7f8f5-b1b3-4176-bc4a-f96265db81c5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":613694,\"team_id\":2,\"email\":\"adelina.petrova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"34f7f8f5-b1b3-4176-bc4a-f96265db81c5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":613694,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"34f7f8f5-b1b3-4176-bc4a-f96265db81c5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613694} {\"correlation_id\":\"34f7f8f5-b1b3-4176-bc4a-f96265db81c5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613694,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"34f7f8f5-b1b3-4176-bc4a-f96265db81c5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":613694,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36} {\"correlation_id\":\"34f7f8f5-b1b3-4176-bc4a-f96265db81c5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":613157,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":350,\"stage_id\":34}} {\"correlation_id\":\"38fd2794-89d3-4ce9-a9fc-f9f8382e30d5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613157} {\"correlation_id\":\"38fd2794-89d3-4ce9-a9fc-f9f8382e30d5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613157,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"38fd2794-89d3-4ce9-a9fc-f9f8382e30d5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":613157,\"participants\":[{\"id\":1000746,\"user_id\":253,\"contact_id\":null,\"lead_id\":null},{\"id\":1000747,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null}]} {\"correlation_id\":\"38fd2794-89d3-4ce9-a9fc-f9f8382e30d5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"38fd2794-89d3-4ce9-a9fc-f9f8382e30d5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"38fd2794-89d3-4ce9-a9fc-f9f8382e30d5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"38fd2794-89d3-4ce9-a9fc-f9f8382e30d5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"38fd2794-89d3-4ce9-a9fc-f9f8382e30d5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":613157,\"team_id\":2,\"email\":\"preslava.ivanova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"38fd2794-89d3-4ce9-a9fc-f9f8382e30d5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"38fd2794-89d3-4ce9-a9fc-f9f8382e30d5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":613157,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"38fd2794-89d3-4ce9-a9fc-f9f8382e30d5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613157} {\"correlation_id\":\"38fd2794-89d3-4ce9-a9fc-f9f8382e30d5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613157,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"38fd2794-89d3-4ce9-a9fc-f9f8382e30d5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":613157,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":350,\"stage_id\":34} {\"correlation_id\":\"38fd2794-89d3-4ce9-a9fc-f9f8382e30d5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":613156,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":350,\"stage_id\":34}} {\"correlation_id\":\"f33d171a-96c9-465c-bcf2-facecea0a7d6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613156} {\"correlation_id\":\"f33d171a-96c9-465c-bcf2-facecea0a7d6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613156,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"f33d171a-96c9-465c-bcf2-facecea0a7d6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":613156,\"participants\":[{\"id\":1000744,\"user_id\":253,\"contact_id\":null,\"lead_id\":null},{\"id\":1000745,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null}]} {\"correlation_id\":\"f33d171a-96c9-465c-bcf2-facecea0a7d6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"f33d171a-96c9-465c-bcf2-facecea0a7d6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"f33d171a-96c9-465c-bcf2-facecea0a7d6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"f33d171a-96c9-465c-bcf2-facecea0a7d6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"f33d171a-96c9-465c-bcf2-facecea0a7d6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":613156,\"team_id\":2,\"email\":\"preslava.ivanova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"f33d171a-96c9-465c-bcf2-facecea0a7d6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"f33d171a-96c9-465c-bcf2-facecea0a7d6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":613156,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"f33d171a-96c9-465c-bcf2-facecea0a7d6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613156} {\"correlation_id\":\"f33d171a-96c9-465c-bcf2-facecea0a7d6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613156,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"f33d171a-96c9-465c-bcf2-facecea0a7d6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":613156,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":350,\"stage_id\":34} {\"correlation_id\":\"f33d171a-96c9-465c-bcf2-facecea0a7d6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":613155,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":350,\"stage_id\":34}} {\"correlation_id\":\"6907203d-5426-446b-b0ed-cb3a79828876\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613155} {\"correlation_id\":\"6907203d-5426-446b-b0ed-cb3a79828876\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613155,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"6907203d-5426-446b-b0ed-cb3a79828876\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":613155,\"participants\":[{\"id\":1000742,\"user_id\":253,\"contact_id\":null,\"lead_id\":null},{\"id\":1000743,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null}]} {\"correlation_id\":\"6907203d-5426-446b-b0ed-cb3a79828876\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"6907203d-5426-446b-b0ed-cb3a79828876\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"6907203d-5426-446b-b0ed-cb3a79828876\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"6907203d-5426-446b-b0ed-cb3a79828876\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"6907203d-5426-446b-b0ed-cb3a79828876\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":613155,\"team_id\":2,\"email\":\"preslava.ivanova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"6907203d-5426-446b-b0ed-cb3a79828876\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"6907203d-5426-446b-b0ed-cb3a79828876\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":613155,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"6907203d-5426-446b-b0ed-cb3a79828876\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613155} {\"correlation_id\":\"6907203d-5426-446b-b0ed-cb3a79828876\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613155,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"6907203d-5426-446b-b0ed-cb3a79828876\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":613155,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":350,\"stage_id\":34} {\"correlation_id\":\"6907203d-5426-446b-b0ed-cb3a79828876\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":613130,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36}} {\"correlation_id\":\"b39eb37f-ecba-45cf-99d3-dc45ee004976\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613130} {\"correlation_id\":\"b39eb37f-ecba-45cf-99d3-dc45ee004976\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613130,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"b39eb37f-ecba-45cf-99d3-dc45ee004976\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":613130,\"participants\":[{\"id\":1000693,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null},{\"id\":1000694,\"user_id\":261,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"b39eb37f-ecba-45cf-99d3-dc45ee004976\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"b39eb37f-ecba-45cf-99d3-dc45ee004976\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"b39eb37f-ecba-45cf-99d3-dc45ee004976\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"b39eb37f-ecba-45cf-99d3-dc45ee004976\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"b39eb37f-ecba-45cf-99d3-dc45ee004976\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"b39eb37f-ecba-45cf-99d3-dc45ee004976\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":613130,\"team_id\":2,\"email\":\"adelina.petrova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"b39eb37f-ecba-45cf-99d3-dc45ee004976\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":613130,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"b39eb37f-ecba-45cf-99d3-dc45ee004976\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613130} {\"correlation_id\":\"b39eb37f-ecba-45cf-99d3-dc45ee004976\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613130,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"b39eb37f-ecba-45cf-99d3-dc45ee004976\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":613130,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36} {\"correlation_id\":\"b39eb37f-ecba-45cf-99d3-dc45ee004976\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612924,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":165,\"stage_id\":89}} {\"correlation_id\":\"22353f3a-e0aa-43eb-9a06-f618d38746e7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612924} {\"correlation_id\":\"22353f3a-e0aa-43eb-9a06-f618d38746e7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612924,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"22353f3a-e0aa-43eb-9a06-f618d38746e7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612924,\"participants\":[{\"id\":1000290,\"user_id\":19,\"contact_id\":null,\"lead_id\":null},{\"id\":1000291,\"user_id\":null,\"contact_id\":97,\"lead_id\":null}]} {\"correlation_id\":\"22353f3a-e0aa-43eb-9a06-f618d38746e7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"22353f3a-e0aa-43eb-9a06-f618d38746e7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"22353f3a-e0aa-43eb-9a06-f618d38746e7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"22353f3a-e0aa-43eb-9a06-f618d38746e7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"22353f3a-e0aa-43eb-9a06-f618d38746e7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612924,\"team_id\":2,\"email\":\"james.graham@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"22353f3a-e0aa-43eb-9a06-f618d38746e7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: ProspectCache - Searching DB for opportunity by owner {\"account_id\":69,\"contact_id\":97,\"owner_id\":19} {\"correlation_id\":\"22353f3a-e0aa-43eb-9a06-f618d38746e7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: ProspectCache - Opportunity DB search results {\"account_id\":69,\"contact_id\":97,\"opportunity_id\":165} {\"correlation_id\":\"22353f3a-e0aa-43eb-9a06-f618d38746e7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinsoncrusoe@test.com\"} {\"correlation_id\":\"22353f3a-e0aa-43eb-9a06-f618d38746e7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612924,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"22353f3a-e0aa-43eb-9a06-f618d38746e7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612924} {\"correlation_id\":\"22353f3a-e0aa-43eb-9a06-f618d38746e7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612924,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"22353f3a-e0aa-43eb-9a06-f618d38746e7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612924,\"remote_search\":true,\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":165,\"stage_id\":89} {\"correlation_id\":\"22353f3a-e0aa-43eb-9a06-f618d38746e7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612923,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":165,\"stage_id\":89}} {\"correlation_id\":\"602cc3c5-932c-4d3d-9370-35217adac6e0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612923} {\"correlation_id\":\"602cc3c5-932c-4d3d-9370-35217adac6e0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612923,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"602cc3c5-932c-4d3d-9370-35217adac6e0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612923,\"participants\":[{\"id\":1000288,\"user_id\":19,\"contact_id\":null,\"lead_id\":null},{\"id\":1000289,\"user_id\":null,\"contact_id\":97,\"lead_id\":null}]} {\"correlation_id\":\"602cc3c5-932c-4d3d-9370-35217adac6e0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"602cc3c5-932c-4d3d-9370-35217adac6e0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"602cc3c5-932c-4d3d-9370-35217adac6e0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"602cc3c5-932c-4d3d-9370-35217adac6e0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"602cc3c5-932c-4d3d-9370-35217adac6e0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612923,\"team_id\":2,\"email\":\"james.graham@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"602cc3c5-932c-4d3d-9370-35217adac6e0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinsoncrusoe@test.com\"} {\"correlation_id\":\"602cc3c5-932c-4d3d-9370-35217adac6e0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612923,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"602cc3c5-932c-4d3d-9370-35217adac6e0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612923} {\"correlation_id\":\"602cc3c5-932c-4d3d-9370-35217adac6e0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612923,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"602cc3c5-932c-4d3d-9370-35217adac6e0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612923,\"remote_search\":true,\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":165,\"stage_id\":89} {\"correlation_id\":\"602cc3c5-932c-4d3d-9370-35217adac6e0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612922,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":165,\"stage_id\":89}} {\"correlation_id\":\"4272a600-7709-4e0b-9ddd-2bf9a93ee051\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612922} {\"correlation_id\":\"4272a600-7709-4e0b-9ddd-2bf9a93ee051\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612922,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"4272a600-7709-4e0b-9ddd-2bf9a93ee051\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612922,\"participants\":[{\"id\":1000286,\"user_id\":19,\"contact_id\":null,\"lead_id\":null},{\"id\":1000287,\"user_id\":null,\"contact_id\":97,\"lead_id\":null}]} {\"correlation_id\":\"4272a600-7709-4e0b-9ddd-2bf9a93ee051\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"4272a600-7709-4e0b-9ddd-2bf9a93ee051\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"4272a600-7709-4e0b-9ddd-2bf9a93ee051\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4272a600-7709-4e0b-9ddd-2bf9a93ee051\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"4272a600-7709-4e0b-9ddd-2bf9a93ee051\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612922,\"team_id\":2,\"email\":\"james.graham@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"4272a600-7709-4e0b-9ddd-2bf9a93ee051\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinsoncrusoe@test.com\"} {\"correlation_id\":\"4272a600-7709-4e0b-9ddd-2bf9a93ee051\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612922,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"4272a600-7709-4e0b-9ddd-2bf9a93ee051\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612922} {\"correlation_id\":\"4272a600-7709-4e0b-9ddd-2bf9a93ee051\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612922,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"4272a600-7709-4e0b-9ddd-2bf9a93ee051\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612922,\"remote_search\":true,\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":165,\"stage_id\":89} {\"correlation_id\":\"4272a600-7709-4e0b-9ddd-2bf9a93ee051\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612847,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36}} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612847} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612847,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612847,\"participants\":[{\"id\":1000130,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":1000131,\"user_id\":261,\"contact_id\":null,\"lead_id\":null},{\"id\":1000151,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null}]} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [Prospect match] Cache miss, calling the API {\"identifier_type\":\"email\",\"identifier\":\"adelina.petrova@jiminny.com\"} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [Hubspot] Failed to fetch contact {\"email\":\"adelina.petrova@jiminny.com\",\"reason\":\"[404] Client error: `GET https://api.hubapi.com/crm/v3/objects/contacts/adelina.petrova%40jiminny.com?properties=email%2Cfirstname%2Clastname%2Ccountry%2Cphone%2Cmobilephone%2Cjobtitle%2Chubspot_owner_id%2Cassociatedcompanyid%2Cphoto&archived=0&idProperty=email` resulted in a `404 Not Found` response\"} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [Prospect match] API returned empty result, caching the miss with empty prospect data {\"identifier_type\":\"email\",\"identifier\":\"adelina.petrova@jiminny.com\"} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:27] local.INFO: [Hubspot] Pagination completed {\"team_id\":2,\"endpoint\":\"https://api.hubapi.com/crm/v3/objects/contact/search\",\"total_requests\":1,\"total_records_fetched\":0,\"total_elapsed_seconds\":0.22,\"average_seconds_per_request\":0.22} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:27] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"adelina.petrova@jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:27] local.INFO: [Prospect match] Cache miss {\"identifier_type\":\"domain\",\"identifier\":\"jiminny.com\",\"crm\":\"hubspot\"} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:27] local.INFO: [Prospect match] Cache miss, calling the API {\"identifier_type\":\"domain\",\"identifier\":\"jiminny.com\"} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:27] local.INFO: [HubSpot] importAccount {\"crm_provider_id\":\"749766179\",\"config_id\":2} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:27] local.INFO: [HubSpot] CRM Search requested {\"request\":{\"filterGroups\":[{\"filters\":[{\"propertyName\":\"associations.company\",\"operator\":\"EQ\",\"value\":\"749766179\"},{\"propertyName\":\"dealstage\",\"operator\":\"NOT_IN\",\"values\":[\"closedwon\",\"4040964\",\"59247967\"]},{\"propertyName\":\"dealstage\",\"operator\":\"NOT_IN\",\"values\":[\"closedlost\",\"4040965\",\"59247968\"]}]}],\"sorts\":[{\"propertyName\":\"modifieddate\",\"direction\":\"DESCENDING\"}],\"properties\":[\"dealname\",\"amount\",\"hubspot_owner_id\",\"pipeline\",\"dealstage\",\"closedate\",\"deal_currency_code\"],\"limit\":200}} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:27] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"6bdeb8f0-c361-4cb3-9fb1-c6bdf466a73e\",\"trace_id\":\"d050352e-9bf5-497a-99d6-e46661a04a04\"}\n[2026-05-11 10:43:27] local.INFO: [Hubspot] Pagination completed {\"team_id\":2,\"endpoint\":\"https://api.hubapi.com/crm/v3/objects/deals/search\",\"total_requests\":1,\"total_records_fetched\":10,\"total_elapsed_seconds\":0.25,\"average_seconds_per_request\":0.25} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:27] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":60.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"6bdeb8f0-c361-4cb3-9fb1-c6bdf466a73e\",\"trace_id\":\"d050352e-9bf5-497a-99d6-e46661a04a04\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612847,\"team_id\":2,\"email\":\"adelina.petrova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612847,\"participants_processed\":3,\"exact_matches\":1,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612847} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612847,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612847,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612822,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36}} {\"correlation_id\":\"a8970854-ad6a-4751-9cc5-ffe651d20f06\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612822} {\"correlation_id\":\"a8970854-ad6a-4751-9cc5-ffe651d20f06\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612822,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"a8970854-ad6a-4751-9cc5-ffe651d20f06\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612822,\"participants\":[{\"id\":1000080,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null},{\"id\":1000081,\"user_id\":261,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"a8970854-ad6a-4751-9cc5-ffe651d20f06\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"a8970854-ad6a-4751-9cc5-ffe651d20f06\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"a8970854-ad6a-4751-9cc5-ffe651d20f06\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"a8970854-ad6a-4751-9cc5-ffe651d20f06\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"a8970854-ad6a-4751-9cc5-ffe651d20f06\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"a8970854-ad6a-4751-9cc5-ffe651d20f06\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612822,\"team_id\":2,\"email\":\"adelina.petrova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"a8970854-ad6a-4751-9cc5-ffe651d20f06\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612822,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"a8970854-ad6a-4751-9cc5-ffe651d20f06\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612822} {\"correlation_id\":\"a8970854-ad6a-4751-9cc5-ffe651d20f06\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612822,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"a8970854-ad6a-4751-9cc5-ffe651d20f06\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612822,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36} {\"correlation_id\":\"a8970854-ad6a-4751-9cc5-ffe651d20f06\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612819,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36}} {\"correlation_id\":\"16e6bc16-5ab8-49ce-b7da-c292b8c94105\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612819} {\"correlation_id\":\"16e6bc16-5ab8-49ce-b7da-c292b8c94105\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612819,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"16e6bc16-5ab8-49ce-b7da-c292b8c94105\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612819,\"participants\":[{\"id\":1000073,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null},{\"id\":1000074,\"user_id\":261,\"contact_id\":null,\"lead_id\":null},{\"id\":1000075,\"user_id\":null,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"16e6bc16-5ab8-49ce-b7da-c292b8c94105\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"16e6bc16-5ab8-49ce-b7da-c292b8c94105\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"16e6bc16-5ab8-49ce-b7da-c292b8c94105\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"16e6bc16-5ab8-49ce-b7da-c292b8c94105\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"16e6bc16-5ab8-49ce-b7da-c292b8c94105\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"16e6bc16-5ab8-49ce-b7da-c292b8c94105\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612819,\"team_id\":2,\"email\":\"adelina.petrova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"16e6bc16-5ab8-49ce-b7da-c292b8c94105\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"adelina.petrova@jiminny.com\"} {\"correlation_id\":\"16e6bc16-5ab8-49ce-b7da-c292b8c94105\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"adelina.petrova@jiminny.com\"} {\"correlation_id\":\"16e6bc16-5ab8-49ce-b7da-c292b8c94105\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"adelina.petrova@jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"16e6bc16-5ab8-49ce-b7da-c292b8c94105\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612819,\"participants_processed\":3,\"exact_matches\":1,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"16e6bc16-5ab8-49ce-b7da-c292b8c94105\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612819} {\"correlation_id\":\"16e6bc16-5ab8-49ce-b7da-c292b8c94105\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612819,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"16e6bc16-5ab8-49ce-b7da-c292b8c94105\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612819,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36} {\"correlation_id\":\"16e6bc16-5ab8-49ce-b7da-c292b8c94105\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612673,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36}} {\"correlation_id\":\"8711a017-62dd-479f-b84e-123d6e3f40be\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612673} {\"correlation_id\":\"8711a017-62dd-479f-b84e-123d6e3f40be\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612673,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"8711a017-62dd-479f-b84e-123d6e3f40be\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612673,\"participants\":[{\"id\":999993,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null},{\"id\":999994,\"user_id\":261,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"8711a017-62dd-479f-b84e-123d6e3f40be\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"8711a017-62dd-479f-b84e-123d6e3f40be\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"8711a017-62dd-479f-b84e-123d6e3f40be\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"8711a017-62dd-479f-b84e-123d6e3f40be\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"8711a017-62dd-479f-b84e-123d6e3f40be\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"8711a017-62dd-479f-b84e-123d6e3f40be\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612673,\"team_id\":2,\"email\":\"adelina.petrova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"8711a017-62dd-479f-b84e-123d6e3f40be\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612673,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"8711a017-62dd-479f-b84e-123d6e3f40be\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612673} {\"correlation_id\":\"8711a017-62dd-479f-b84e-123d6e3f40be\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612673,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"8711a017-62dd-479f-b84e-123d6e3f40be\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612673,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36} {\"correlation_id\":\"8711a017-62dd-479f-b84e-123d6e3f40be\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612642,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36}} {\"correlation_id\":\"aa7b8829-1739-4f9b-aad1-c6868d8679ca\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612642} {\"correlation_id\":\"aa7b8829-1739-4f9b-aad1-c6868d8679ca\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612642,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"aa7b8829-1739-4f9b-aad1-c6868d8679ca\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612642,\"participants\":[{\"id\":999935,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null},{\"id\":999936,\"user_id\":261,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"aa7b8829-1739-4f9b-aad1-c6868d8679ca\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"aa7b8829-1739-4f9b-aad1-c6868d8679ca\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"aa7b8829-1739-4f9b-aad1-c6868d8679ca\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"aa7b8829-1739-4f9b-aad1-c6868d8679ca\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"aa7b8829-1739-4f9b-aad1-c6868d8679ca\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"aa7b8829-1739-4f9b-aad1-c6868d8679ca\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612642,\"team_id\":2,\"email\":\"adelina.petrova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"aa7b8829-1739-4f9b-aad1-c6868d8679ca\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612642,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"aa7b8829-1739-4f9b-aad1-c6868d8679ca\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612642} {\"correlation_id\":\"aa7b8829-1739-4f9b-aad1-c6868d8679ca\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612642,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"aa7b8829-1739-4f9b-aad1-c6868d8679ca\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612642,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36} {\"correlation_id\":\"aa7b8829-1739-4f9b-aad1-c6868d8679ca\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612598,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"2fef3119-c9e0-421d-b77e-3cf1e26b480a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612598} {\"correlation_id\":\"2fef3119-c9e0-421d-b77e-3cf1e26b480a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612598,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"2fef3119-c9e0-421d-b77e-3cf1e26b480a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612598,\"participants\":[{\"id\":999857,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null},{\"id\":999858,\"user_id\":206,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"2fef3119-c9e0-421d-b77e-3cf1e26b480a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"2fef3119-c9e0-421d-b77e-3cf1e26b480a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"2fef3119-c9e0-421d-b77e-3cf1e26b480a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"2fef3119-c9e0-421d-b77e-3cf1e26b480a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"2fef3119-c9e0-421d-b77e-3cf1e26b480a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: ProspectCache - Searching DB for opportunity by owner {\"account_id\":243,\"contact_id\":4491,\"owner_id\":206} {\"correlation_id\":\"2fef3119-c9e0-421d-b77e-3cf1e26b480a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: ProspectCache - Fallback DB opportunity search {\"account_id\":243,\"contact_id\":4491} {\"correlation_id\":\"2fef3119-c9e0-421d-b77e-3cf1e26b480a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: ProspectCache - Opportunity DB search results {\"account_id\":243,\"contact_id\":4491,\"opportunity_id\":276} {\"correlation_id\":\"2fef3119-c9e0-421d-b77e-3cf1e26b480a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"2fef3119-c9e0-421d-b77e-3cf1e26b480a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612598,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"2fef3119-c9e0-421d-b77e-3cf1e26b480a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612598,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"2fef3119-c9e0-421d-b77e-3cf1e26b480a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612598} {\"correlation_id\":\"2fef3119-c9e0-421d-b77e-3cf1e26b480a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612598,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"2fef3119-c9e0-421d-b77e-3cf1e26b480a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612598,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"2fef3119-c9e0-421d-b77e-3cf1e26b480a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612597,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"f4aa8c68-1048-465c-8ab6-e414220b2b3d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612597} {\"correlation_id\":\"f4aa8c68-1048-465c-8ab6-e414220b2b3d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612597,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"f4aa8c68-1048-465c-8ab6-e414220b2b3d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612597,\"participants\":[{\"id\":999855,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999856,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null}]} {\"correlation_id\":\"f4aa8c68-1048-465c-8ab6-e414220b2b3d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"f4aa8c68-1048-465c-8ab6-e414220b2b3d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"f4aa8c68-1048-465c-8ab6-e414220b2b3d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"f4aa8c68-1048-465c-8ab6-e414220b2b3d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"f4aa8c68-1048-465c-8ab6-e414220b2b3d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612597,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"f4aa8c68-1048-465c-8ab6-e414220b2b3d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"f4aa8c68-1048-465c-8ab6-e414220b2b3d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612597,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"f4aa8c68-1048-465c-8ab6-e414220b2b3d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612597} {\"correlation_id\":\"f4aa8c68-1048-465c-8ab6-e414220b2b3d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612597,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"f4aa8c68-1048-465c-8ab6-e414220b2b3d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612597,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"f4aa8c68-1048-465c-8ab6-e414220b2b3d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612596,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"885d60db-2622-4f69-9826-b15464181dcb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612596} {\"correlation_id\":\"885d60db-2622-4f69-9826-b15464181dcb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612596,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"885d60db-2622-4f69-9826-b15464181dcb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612596,\"participants\":[{\"id\":999853,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999854,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null}]} {\"correlation_id\":\"885d60db-2622-4f69-9826-b15464181dcb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"885d60db-2622-4f69-9826-b15464181dcb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"885d60db-2622-4f69-9826-b15464181dcb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"885d60db-2622-4f69-9826-b15464181dcb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"885d60db-2622-4f69-9826-b15464181dcb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612596,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"885d60db-2622-4f69-9826-b15464181dcb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"885d60db-2622-4f69-9826-b15464181dcb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612596,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"885d60db-2622-4f69-9826-b15464181dcb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612596} {\"correlation_id\":\"885d60db-2622-4f69-9826-b15464181dcb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612596,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"885d60db-2622-4f69-9826-b15464181dcb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612596,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"885d60db-2622-4f69-9826-b15464181dcb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612595,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"0bc8a5e9-778b-4d99-bf0c-d903bd129c55\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612595} {\"correlation_id\":\"0bc8a5e9-778b-4d99-bf0c-d903bd129c55\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612595,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"0bc8a5e9-778b-4d99-bf0c-d903bd129c55\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612595,\"participants\":[{\"id\":999851,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null},{\"id\":999852,\"user_id\":206,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"0bc8a5e9-778b-4d99-bf0c-d903bd129c55\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"0bc8a5e9-778b-4d99-bf0c-d903bd129c55\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"0bc8a5e9-778b-4d99-bf0c-d903bd129c55\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"0bc8a5e9-778b-4d99-bf0c-d903bd129c55\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"0bc8a5e9-778b-4d99-bf0c-d903bd129c55\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"0bc8a5e9-778b-4d99-bf0c-d903bd129c55\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612595,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"0bc8a5e9-778b-4d99-bf0c-d903bd129c55\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612595,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"0bc8a5e9-778b-4d99-bf0c-d903bd129c55\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612595} {\"correlation_id\":\"0bc8a5e9-778b-4d99-bf0c-d903bd129c55\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612595,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"0bc8a5e9-778b-4d99-bf0c-d903bd129c55\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612595,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"0bc8a5e9-778b-4d99-bf0c-d903bd129c55\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612594,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"9710b4b5-e8f1-4fbf-b865-162ea340046c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612594} {\"correlation_id\":\"9710b4b5-e8f1-4fbf-b865-162ea340046c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612594,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"9710b4b5-e8f1-4fbf-b865-162ea340046c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612594,\"participants\":[{\"id\":999849,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999850,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null}]} {\"correlation_id\":\"9710b4b5-e8f1-4fbf-b865-162ea340046c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"9710b4b5-e8f1-4fbf-b865-162ea340046c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"9710b4b5-e8f1-4fbf-b865-162ea340046c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"9710b4b5-e8f1-4fbf-b865-162ea340046c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"9710b4b5-e8f1-4fbf-b865-162ea340046c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612594,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"9710b4b5-e8f1-4fbf-b865-162ea340046c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"9710b4b5-e8f1-4fbf-b865-162ea340046c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612594,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"9710b4b5-e8f1-4fbf-b865-162ea340046c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612594} {\"correlation_id\":\"9710b4b5-e8f1-4fbf-b865-162ea340046c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612594,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"9710b4b5-e8f1-4fbf-b865-162ea340046c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612594,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"9710b4b5-e8f1-4fbf-b865-162ea340046c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612593,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"59a8137b-0518-4c91-b63d-c794df57a8c1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612593} {\"correlation_id\":\"59a8137b-0518-4c91-b63d-c794df57a8c1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612593,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"59a8137b-0518-4c91-b63d-c794df57a8c1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612593,\"participants\":[{\"id\":999847,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999848,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null}]} {\"correlation_id\":\"59a8137b-0518-4c91-b63d-c794df57a8c1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"59a8137b-0518-4c91-b63d-c794df57a8c1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"59a8137b-0518-4c91-b63d-c794df57a8c1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"59a8137b-0518-4c91-b63d-c794df57a8c1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"59a8137b-0518-4c91-b63d-c794df57a8c1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612593,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"59a8137b-0518-4c91-b63d-c794df57a8c1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"59a8137b-0518-4c91-b63d-c794df57a8c1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612593,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"59a8137b-0518-4c91-b63d-c794df57a8c1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612593} {\"correlation_id\":\"59a8137b-0518-4c91-b63d-c794df57a8c1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612593,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"59a8137b-0518-4c91-b63d-c794df57a8c1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612593,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"59a8137b-0518-4c91-b63d-c794df57a8c1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612592,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"e1f2c9b0-a985-4277-b0da-5190fb9b25d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612592} {\"correlation_id\":\"e1f2c9b0-a985-4277-b0da-5190fb9b25d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612592,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"e1f2c9b0-a985-4277-b0da-5190fb9b25d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612592,\"participants\":[{\"id\":999845,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null},{\"id\":999846,\"user_id\":206,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"e1f2c9b0-a985-4277-b0da-5190fb9b25d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"e1f2c9b0-a985-4277-b0da-5190fb9b25d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"e1f2c9b0-a985-4277-b0da-5190fb9b25d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"e1f2c9b0-a985-4277-b0da-5190fb9b25d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"e1f2c9b0-a985-4277-b0da-5190fb9b25d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"e1f2c9b0-a985-4277-b0da-5190fb9b25d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612592,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"e1f2c9b0-a985-4277-b0da-5190fb9b25d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612592,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"e1f2c9b0-a985-4277-b0da-5190fb9b25d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612592} {\"correlation_id\":\"e1f2c9b0-a985-4277-b0da-5190fb9b25d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612592,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"e1f2c9b0-a985-4277-b0da-5190fb9b25d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612592,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"e1f2c9b0-a985-4277-b0da-5190fb9b25d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612591,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"8a5f33ea-490a-41e3-b71a-872c52626583\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612591} {\"correlation_id\":\"8a5f33ea-490a-41e3-b71a-872c52626583\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612591,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"8a5f33ea-490a-41e3-b71a-872c52626583\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612591,\"participants\":[{\"id\":999843,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999844,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null}]} {\"correlation_id\":\"8a5f33ea-490a-41e3-b71a-872c52626583\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"8a5f33ea-490a-41e3-b71a-872c52626583\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"8a5f33ea-490a-41e3-b71a-872c52626583\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"8a5f33ea-490a-41e3-b71a-872c52626583\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"8a5f33ea-490a-41e3-b71a-872c52626583\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612591,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"8a5f33ea-490a-41e3-b71a-872c52626583\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"8a5f33ea-490a-41e3-b71a-872c52626583\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612591,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"8a5f33ea-490a-41e3-b71a-872c52626583\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612591} {\"correlation_id\":\"8a5f33ea-490a-41e3-b71a-872c52626583\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612591,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"8a5f33ea-490a-41e3-b71a-872c52626583\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612591,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"8a5f33ea-490a-41e3-b71a-872c52626583\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612590,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"b34fb574-2229-4ff9-b572-7e2f801295a8\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612590} {\"correlation_id\":\"b34fb574-2229-4ff9-b572-7e2f801295a8\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612590,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"b34fb574-2229-4ff9-b572-7e2f801295a8\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612590,\"participants\":[{\"id\":999841,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999842,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null}]} {\"correlation_id\":\"b34fb574-2229-4ff9-b572-7e2f801295a8\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"b34fb574-2229-4ff9-b572-7e2f801295a8\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"b34fb574-2229-4ff9-b572-7e2f801295a8\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"b34fb574-2229-4ff9-b572-7e2f801295a8\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"b34fb574-2229-4ff9-b572-7e2f801295a8\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612590,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"b34fb574-2229-4ff9-b572-7e2f801295a8\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"b34fb574-2229-4ff9-b572-7e2f801295a8\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612590,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"b34fb574-2229-4ff9-b572-7e2f801295a8\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612590} {\"correlation_id\":\"b34fb574-2229-4ff9-b572-7e2f801295a8\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612590,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"b34fb574-2229-4ff9-b572-7e2f801295a8\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612590,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"b34fb574-2229-4ff9-b572-7e2f801295a8\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612589,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"746b5162-98d0-4e35-96ca-f6756e9b2d39\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612589} {\"correlation_id\":\"746b5162-98d0-4e35-96ca-f6756e9b2d39\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612589,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"746b5162-98d0-4e35-96ca-f6756e9b2d39\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612589,\"participants\":[{\"id\":999839,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999840,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null}]} {\"correlation_id\":\"746b5162-98d0-4e35-96ca-f6756e9b2d39\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"746b5162-98d0-4e35-96ca-f6756e9b2d39\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"746b5162-98d0-4e35-96ca-f6756e9b2d39\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"746b5162-98d0-4e35-96ca-f6756e9b2d39\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"746b5162-98d0-4e35-96ca-f6756e9b2d39\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612589,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"746b5162-98d0-4e35-96ca-f6756e9b2d39\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"746b5162-98d0-4e35-96ca-f6756e9b2d39\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612589,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"746b5162-98d0-4e35-96ca-f6756e9b2d39\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612589} {\"correlation_id\":\"746b5162-98d0-4e35-96ca-f6756e9b2d39\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612589,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"746b5162-98d0-4e35-96ca-f6756e9b2d39\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612589,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"746b5162-98d0-4e35-96ca-f6756e9b2d39\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612588,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"601702ec-6d8b-4aa3-b7e2-780e758b68d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612588} {\"correlation_id\":\"601702ec-6d8b-4aa3-b7e2-780e758b68d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612588,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"601702ec-6d8b-4aa3-b7e2-780e758b68d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612588,\"participants\":[{\"id\":999837,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null},{\"id\":999838,\"user_id\":206,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"601702ec-6d8b-4aa3-b7e2-780e758b68d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"601702ec-6d8b-4aa3-b7e2-780e758b68d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"601702ec-6d8b-4aa3-b7e2-780e758b68d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"601702ec-6d8b-4aa3-b7e2-780e758b68d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"601702ec-6d8b-4aa3-b7e2-780e758b68d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"601702ec-6d8b-4aa3-b7e2-780e758b68d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612588,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"601702ec-6d8b-4aa3-b7e2-780e758b68d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612588,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"601702ec-6d8b-4aa3-b7e2-780e758b68d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612588} {\"correlation_id\":\"601702ec-6d8b-4aa3-b7e2-780e758b68d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612588,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"601702ec-6d8b-4aa3-b7e2-780e758b68d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612588,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"601702ec-6d8b-4aa3-b7e2-780e758b68d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612587,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"75f31c21-1a08-4a7a-beef-a6ee82905eb2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612587} {\"correlation_id\":\"75f31c21-1a08-4a7a-beef-a6ee82905eb2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612587,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"75f31c21-1a08-4a7a-beef-a6ee82905eb2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612587,\"participants\":[{\"id\":999835,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999836,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null}]} {\"correlation_id\":\"75f31c21-1a08-4a7a-beef-a6ee82905eb2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"75f31c21-1a08-4a7a-beef-a6ee82905eb2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"75f31c21-1a08-4a7a-beef-a6ee82905eb2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"75f31c21-1a08-4a7a-beef-a6ee82905eb2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"75f31c21-1a08-4a7a-beef-a6ee82905eb2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612587,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"75f31c21-1a08-4a7a-beef-a6ee82905eb2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"75f31c21-1a08-4a7a-beef-a6ee82905eb2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612587,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"75f31c21-1a08-4a7a-beef-a6ee82905eb2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612587} {\"correlation_id\":\"75f31c21-1a08-4a7a-beef-a6ee82905eb2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612587,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"75f31c21-1a08-4a7a-beef-a6ee82905eb2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612587,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"75f31c21-1a08-4a7a-beef-a6ee82905eb2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612586,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"b79deb62-f26f-4831-8192-2fca40cc0445\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612586} {\"correlation_id\":\"b79deb62-f26f-4831-8192-2fca40cc0445\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612586,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"b79deb62-f26f-4831-8192-2fca40cc0445\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612586,\"participants\":[{\"id\":999833,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null},{\"id\":999834,\"user_id\":206,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"b79deb62-f26f-4831-8192-2fca40cc0445\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"b79deb62-f26f-4831-8192-2fca40cc0445\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"b79deb62-f26f-4831-8192-2fca40cc0445\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"b79deb62-f26f-4831-8192-2fca40cc0445\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"b79deb62-f26f-4831-8192-2fca40cc0445\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"b79deb62-f26f-4831-8192-2fca40cc0445\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612586,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"b79deb62-f26f-4831-8192-2fca40cc0445\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612586,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"b79deb62-f26f-4831-8192-2fca40cc0445\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612586} {\"correlation_id\":\"b79deb62-f26f-4831-8192-2fca40cc0445\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612586,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"b79deb62-f26f-4831-8192-2fca40cc0445\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612586,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"b79deb62-f26f-4831-8192-2fca40cc0445\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612585,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"2aa37843-11f3-4f11-a9ef-3277ddd89ec3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612585} {\"correlation_id\":\"2aa37843-11f3-4f11-a9ef-3277ddd89ec3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612585,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"2aa37843-11f3-4f11-a9ef-3277ddd89ec3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612585,\"participants\":[{\"id\":999831,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999832,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null}]} {\"correlation_id\":\"2aa37843-11f3-4f11-a9ef-3277ddd89ec3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"2aa37843-11f3-4f11-a9ef-3277ddd89ec3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"2aa37843-11f3-4f11-a9ef-3277ddd89ec3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"2aa37843-11f3-4f11-a9ef-3277ddd89ec3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"2aa37843-11f3-4f11-a9ef-3277ddd89ec3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612585,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"2aa37843-11f3-4f11-a9ef-3277ddd89ec3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"2aa37843-11f3-4f11-a9ef-3277ddd89ec3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612585,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"2aa37843-11f3-4f11-a9ef-3277ddd89ec3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612585} {\"correlation_id\":\"2aa37843-11f3-4f11-a9ef-3277ddd89ec3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612585,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"2aa37843-11f3-4f11-a9ef-3277ddd89ec3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612585,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"2aa37843-11f3-4f11-a9ef-3277ddd89ec3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612584,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"07bfbf03-ba7c-47ad-8252-88913ec31644\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612584} {\"correlation_id\":\"07bfbf03-ba7c-47ad-8252-88913ec31644\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612584,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"07bfbf03-ba7c-47ad-8252-88913ec31644\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612584,\"participants\":[{\"id\":999829,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999830,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null}]} {\"correlation_id\":\"07bfbf03-ba7c-47ad-8252-88913ec31644\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"07bfbf03-ba7c-47ad-8252-88913ec31644\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"07bfbf03-ba7c-47ad-8252-88913ec31644\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"07bfbf03-ba7c-47ad-8252-88913ec31644\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"07bfbf03-ba7c-47ad-8252-88913ec31644\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612584,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"07bfbf03-ba7c-47ad-8252-88913ec31644\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"07bfbf03-ba7c-47ad-8252-88913ec31644\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612584,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"07bfbf03-ba7c-47ad-8252-88913ec31644\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612584} {\"correlation_id\":\"07bfbf03-ba7c-47ad-8252-88913ec31644\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612584,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"07bfbf03-ba7c-47ad-8252-88913ec31644\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612584,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"07bfbf03-ba7c-47ad-8252-88913ec31644\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612583,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"64e9fc7f-c17e-4af3-b884-54a2c388e8f0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612583} {\"correlation_id\":\"64e9fc7f-c17e-4af3-b884-54a2c388e8f0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612583,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"64e9fc7f-c17e-4af3-b884-54a2c388e8f0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612583,\"participants\":[{\"id\":999827,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null},{\"id\":999828,\"user_id\":206,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"64e9fc7f-c17e-4af3-b884-54a2c388e8f0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"64e9fc7f-c17e-4af3-b884-54a2c388e8f0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"64e9fc7f-c17e-4af3-b884-54a2c388e8f0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"64e9fc7f-c17e-4af3-b884-54a2c388e8f0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"64e9fc7f-c17e-4af3-b884-54a2c388e8f0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"64e9fc7f-c17e-4af3-b884-54a2c388e8f0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612583,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"64e9fc7f-c17e-4af3-b884-54a2c388e8f0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612583,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"64e9fc7f-c17e-4af3-b884-54a2c388e8f0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612583} {\"correlation_id\":\"64e9fc7f-c17e-4af3-b884-54a2c388e8f0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612583,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"64e9fc7f-c17e-4af3-b884-54a2c388e8f0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612583,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"64e9fc7f-c17e-4af3-b884-54a2c388e8f0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612582,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"23188089-fcae-491d-b35d-c20491564cfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612582} {\"correlation_id\":\"23188089-fcae-491d-b35d-c20491564cfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612582,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"23188089-fcae-491d-b35d-c20491564cfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612582,\"participants\":[{\"id\":999825,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999826,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null}]} {\"correlation_id\":\"23188089-fcae-491d-b35d-c20491564cfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"23188089-fcae-491d-b35d-c20491564cfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"23188089-fcae-491d-b35d-c20491564cfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"23188089-fcae-491d-b35d-c20491564cfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"23188089-fcae-491d-b35d-c20491564cfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612582,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"23188089-fcae-491d-b35d-c20491564cfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"23188089-fcae-491d-b35d-c20491564cfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612582,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"23188089-fcae-491d-b35d-c20491564cfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612582} {\"correlation_id\":\"23188089-fcae-491d-b35d-c20491564cfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612582,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"23188089-fcae-491d-b35d-c20491564cfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612582,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"23188089-fcae-491d-b35d-c20491564cfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"9f9cc209-aee5-4d58-b4ad-1455693e83d6\",\"trace_id\":\"f11e2691-d834-4325-91f7-003e08888b34\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612581,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"91305be5-0942-40ad-86c8-c2b2f018ea2b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612581} {\"correlation_id\":\"91305be5-0942-40ad-86c8-c2b2f018ea2b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612581,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"91305be5-0942-40ad-86c8-c2b2f018ea2b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612581,\"participants\":[{\"id\":999823,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null},{\"id\":999824,\"user_id\":206,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"91305be5-0942-40ad-86c8-c2b2f018ea2b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"91305be5-0942-40ad-86c8-c2b2f018ea2b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"91305be5-0942-40ad-86c8-c2b2f018ea2b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"91305be5-0942-40ad-86c8-c2b2f018ea2b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"91305be5-0942-40ad-86c8-c2b2f018ea2b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"91305be5-0942-40ad-86c8-c2b2f018ea2b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612581,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"91305be5-0942-40ad-86c8-c2b2f018ea2b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612581,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"91305be5-0942-40ad-86c8-c2b2f018ea2b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612581} {\"correlation_id\":\"91305be5-0942-40ad-86c8-c2b2f018ea2b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612581,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"91305be5-0942-40ad-86c8-c2b2f018ea2b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [EmailSchedule] STARTING batch process {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"9f9cc209-aee5-4d58-b4ad-1455693e83d6\",\"trace_id\":\"f11e2691-d834-4325-91f7-003e08888b34\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612581,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"91305be5-0942-40ad-86c8-c2b2f018ea2b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612565,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"be6ed1a4-4495-4ec9-932f-da6698c31cec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [EmailSchedule] FINISHED batch process {\"host\":\"docker_lamp_1\",\"processed\":0} {\"correlation_id\":\"9f9cc209-aee5-4d58-b4ad-1455693e83d6\",\"trace_id\":\"f11e2691-d834-4325-91f7-003e08888b34\"}\n[2026-05-11 10:43:29] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":60.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"9f9cc209-aee5-4d58-b4ad-1455693e83d6\",\"trace_id\":\"f11e2691-d834-4325-91f7-003e08888b34\"}\n[2026-05-11 10:43:29] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612565} {\"correlation_id\":\"be6ed1a4-4495-4ec9-932f-da6698c31cec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612565,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"be6ed1a4-4495-4ec9-932f-da6698c31cec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612565,\"participants\":[{\"id\":999789,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999790,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null}]} {\"correlation_id\":\"be6ed1a4-4495-4ec9-932f-da6698c31cec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"be6ed1a4-4495-4ec9-932f-da6698c31cec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"be6ed1a4-4495-4ec9-932f-da6698c31cec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"be6ed1a4-4495-4ec9-932f-da6698c31cec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"be6ed1a4-4495-4ec9-932f-da6698c31cec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612565,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"be6ed1a4-4495-4ec9-932f-da6698c31cec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"be6ed1a4-4495-4ec9-932f-da6698c31cec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612565,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"be6ed1a4-4495-4ec9-932f-da6698c31cec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612565} {\"correlation_id\":\"be6ed1a4-4495-4ec9-932f-da6698c31cec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612565,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"be6ed1a4-4495-4ec9-932f-da6698c31cec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612565,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"be6ed1a4-4495-4ec9-932f-da6698c31cec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612563,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":350,\"stage_id\":34}} {\"correlation_id\":\"77b39c9c-1e21-4c1d-925b-4acce1b204d3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612563} {\"correlation_id\":\"77b39c9c-1e21-4c1d-925b-4acce1b204d3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612563,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"77b39c9c-1e21-4c1d-925b-4acce1b204d3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612563,\"participants\":[{\"id\":999784,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999785,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null}]} {\"correlation_id\":\"77b39c9c-1e21-4c1d-925b-4acce1b204d3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"77b39c9c-1e21-4c1d-925b-4acce1b204d3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"77b39c9c-1e21-4c1d-925b-4acce1b204d3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"77b39c9c-1e21-4c1d-925b-4acce1b204d3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"77b39c9c-1e21-4c1d-925b-4acce1b204d3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612563,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"77b39c9c-1e21-4c1d-925b-4acce1b204d3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: ProspectCache - Searching DB for opportunity by owner {\"account_id\":244,\"contact_id\":4487,\"owner_id\":206} {\"correlation_id\":\"77b39c9c-1e21-4c1d-925b-4acce1b204d3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: ProspectCache - Fallback DB opportunity search {\"account_id\":244,\"contact_id\":4487} {\"correlation_id\":\"77b39c9c-1e21-4c1d-925b-4acce1b204d3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: ProspectCache - Opportunity DB search results {\"account_id\":244,\"contact_id\":4487,\"opportunity_id\":350} {\"correlation_id\":\"77b39c9c-1e21-4c1d-925b-4acce1b204d3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"77b39c9c-1e21-4c1d-925b-4acce1b204d3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:30] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612563,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"77b39c9c-1e21-4c1d-925b-4acce1b204d3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:30] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612563} {\"correlation_id\":\"77b39c9c-1e21-4c1d-925b-4acce1b204d3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:30] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612563,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"77b39c9c-1e21-4c1d-925b-4acce1b204d3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:30] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612563,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":350,\"stage_id\":34} {\"correlation_id\":\"77b39c9c-1e21-4c1d-925b-4acce1b204d3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:30] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612562,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"485e0be1-a971-4cde-9ead-4276768bdf66\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:30] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612562} {\"correlation_id\":\"485e0be1-a971-4cde-9ead-4276768bdf66\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:30] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612562,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"485e0be1-a971-4cde-9ead-4276768bdf66\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:30] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612562,\"participants\":[{\"id\":999782,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":999783,\"user_id\":206,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"485e0be1-a971-4cde-9ead-4276768bdf66\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:30] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"485e0be1-a971-4cde-9ead-4276768bdf66\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:30] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"485e0be1-a971-4cde-9ead-4276768bdf66\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:30] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"485e0be1-a971-4cde-9ead-4276768bdf66\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:30] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"485e0be1-a971-4cde-9ead-4276768bdf66\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:30] local.INFO: [Prospect match] Cache miss, calling the API {\"identifier_type\":\"email\",\"identifier\":\"447782589921@txt.staging.jiminny.com\"} {\"correlation_id\":\"485e0be1-a971-4cde-9ead-4276768bdf66\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:30] local.INFO: [Hubspot] Failed to fetch contact {\"email\":\"447782589921@txt.staging.jiminny.com\",\"reason\":\"[404] Client error: `GET https://api.hubapi.com/crm/v3/objects/contacts/447782589921%40txt.staging.jiminny.com?properties=email%2Cfirstname%2Clastname%2Ccountry%2Cphone%2Cmobilephone%2Cjobtitle%2Chubspot_owner_id%2Cassociatedcompanyid%2Cphoto&archived=0&idProperty=email` resulted in a `404 Not Found` response\"} {\"correlation_id\":\"485e0be1-a971-4cde-9ead-4276768bdf66\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:30] local.INFO: [Prospect match] API returned empty result, caching the miss with empty prospect data {\"identifier_type\":\"email\",\"identifier\":\"447782589921@txt.staging.jiminny.com\"} {\"correlation_id\":\"485e0be1-a971-4cde-9ead-4276768bdf66\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:30] local.INFO: [Hubspot] Pagination completed {\"team_id\":2,\"endpoint\":\"https://api.hubapi.com/crm/v3/objects/contact/search\",\"total_requests\":1,\"total_records_fetched\":0,\"total_elapsed_seconds\":0.22,\"average_seconds_per_request\":0.22} {\"correlation_id\":\"485e0be1-a971-4cde-9ead-4276768bdf66\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:30] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"447782589921@txt.staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"485e0be1-a971-4cde-9ead-4276768bdf66\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:30] local.INFO: [Prospect match] Cache miss {\"identifier_type\":\"domain\",\"identifier\":\"jiminny.com\",\"crm\":\"hubspot\"} {\"correlation_id\":\"485e0be1-a971-4cde-9ead-4276768bdf66\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:30] local.INFO: [Prospect match] Cache miss, calling the API {\"identifier_type\":\"domain\",\"identifier\":\"jiminny.com\"} {\"correlation_id\":\"485e0be1-a971-4cde-9ead-4276768bdf66\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:31] local.INFO: [HubSpot] importAccount {\"crm_provider_id\":\"749766179\",\"config_id\":2} {\"correlation_id\":\"485e0be1-a971-4cde-9ead-4276768bdf66\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:31] local.INFO: [HubSpot] CRM Search requested {\"request\":{\"filterGroups\":[{\"filters\":[{\"propertyName\":\"associations.company\",\"operator\":\"EQ\",\"value\":\"749766179\"},{\"propertyName\":\"dealstage\",\"operator\":\"NOT_IN\",\"values\":[\"closedwon\",\"4040964\",\"59247967\"]},{\"propertyName\":\"dealstage\",\"operator\":\"NOT_IN\",\"values\":[\"closedlost\",\"4040965\",\"59247968\"]}]}],\"sorts\":[{\"propertyName\":\"modifieddate\",\"direction\":\"DESCENDING\"}],\"properties\":[\"dealname\",\"amount\",\"hubspot_owner_id\",\"pipeline\",\"dealstage\",\"closedate\",\"deal_currency_code\"],\"limit\":200}} {\"correlation_id\":\"485e0be1-a971-4cde-9ead-4276768bdf66\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:31] local.INFO: [Hubspot] Pagination completed {\"team_id\":2,\"endpoint\":\"https://api.hubapi.com/crm/v3/objects/deals/search\",\"total_requests\":1,\"total_records_fetched\":10,\"total_elapsed_seconds\":0.3,\"average_seconds_per_request\":0.3} {\"correlation_id\":\"485e0be1-a971-4cde-9ead-4276768bdf66\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612562,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"485e0be1-a971-4cde-9ead-4276768bdf66\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612562,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"485e0be1-a971-4cde-9ead-4276768bdf66\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612562} {\"correlation_id\":\"485e0be1-a971-4cde-9ead-4276768bdf66\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612562,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"485e0be1-a971-4cde-9ead-4276768bdf66\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612562,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"485e0be1-a971-4cde-9ead-4276768bdf66\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612561,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"9fffce69-30ed-4c66-9ee9-e980e776cd57\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612561} {\"correlation_id\":\"9fffce69-30ed-4c66-9ee9-e980e776cd57\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612561,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"9fffce69-30ed-4c66-9ee9-e980e776cd57\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612561,\"participants\":[{\"id\":999780,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999781,\"user_id\":null,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"9fffce69-30ed-4c66-9ee9-e980e776cd57\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"9fffce69-30ed-4c66-9ee9-e980e776cd57\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"9fffce69-30ed-4c66-9ee9-e980e776cd57\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"9fffce69-30ed-4c66-9ee9-e980e776cd57\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"9fffce69-30ed-4c66-9ee9-e980e776cd57\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612561,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"9fffce69-30ed-4c66-9ee9-e980e776cd57\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [Prospect match] Cache miss, calling the API {\"identifier_type\":\"email\",\"identifier\":\"447700174614.447782589921.OeREojLVnk@txt.staging.jiminny.com\"} {\"correlation_id\":\"9fffce69-30ed-4c66-9ee9-e980e776cd57\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [Hubspot] Failed to fetch contact {\"email\":\"447700174614.447782589921.OeREojLVnk@txt.staging.jiminny.com\",\"reason\":\"[404] Client error: `GET https://api.hubapi.com/crm/v3/objects/contacts/447700174614.447782589921.OeREojLVnk%40txt.staging.jiminny.com?properties=email%2Cfirstname%2Clastname%2Ccountry%2Cphone%2Cmobilephone%2Cjobtitle%2Chubspot_owner_id%2Cassociatedcompanyid%2Cphoto&archived=0&idProperty=email` resulted in a `404 Not Found` response\"} {\"correlation_id\":\"9fffce69-30ed-4c66-9ee9-e980e776cd57\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [Prospect match] API returned empty result, caching the miss with empty prospect data {\"identifier_type\":\"email\",\"identifier\":\"447700174614.447782589921.OeREojLVnk@txt.staging.jiminny.com\"} {\"correlation_id\":\"9fffce69-30ed-4c66-9ee9-e980e776cd57\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"447700174614.447782589921.OeREojLVnk@txt.staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"9fffce69-30ed-4c66-9ee9-e980e776cd57\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612561,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"9fffce69-30ed-4c66-9ee9-e980e776cd57\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612561} {\"correlation_id\":\"9fffce69-30ed-4c66-9ee9-e980e776cd57\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612561,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"9fffce69-30ed-4c66-9ee9-e980e776cd57\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612561,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"9fffce69-30ed-4c66-9ee9-e980e776cd57\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612560,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"6a946453-4ffe-4df1-b91d-f73baf850a7a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612560} {\"correlation_id\":\"6a946453-4ffe-4df1-b91d-f73baf850a7a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612560,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"6a946453-4ffe-4df1-b91d-f73baf850a7a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612560,\"participants\":[{\"id\":999778,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":999779,\"user_id\":206,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"6a946453-4ffe-4df1-b91d-f73baf850a7a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"6a946453-4ffe-4df1-b91d-f73baf850a7a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"6a946453-4ffe-4df1-b91d-f73baf850a7a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"6a946453-4ffe-4df1-b91d-f73baf850a7a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"6a946453-4ffe-4df1-b91d-f73baf850a7a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"447782589921@txt.staging.jiminny.com\"} {\"correlation_id\":\"6a946453-4ffe-4df1-b91d-f73baf850a7a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"447782589921@txt.staging.jiminny.com\"} {\"correlation_id\":\"6a946453-4ffe-4df1-b91d-f73baf850a7a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"447782589921@txt.staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"6a946453-4ffe-4df1-b91d-f73baf850a7a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612560,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"6a946453-4ffe-4df1-b91d-f73baf850a7a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612560,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"6a946453-4ffe-4df1-b91d-f73baf850a7a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612560} {\"correlation_id\":\"6a946453-4ffe-4df1-b91d-f73baf850a7a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612560,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"6a946453-4ffe-4df1-b91d-f73baf850a7a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612560,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"6a946453-4ffe-4df1-b91d-f73baf850a7a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612559,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":5011,\"stage_id\":34}} {\"correlation_id\":\"6231af67-0852-4f81-a268-953b6db0b3aa\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612559} {\"correlation_id\":\"6231af67-0852-4f81-a268-953b6db0b3aa\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612559,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"6231af67-0852-4f81-a268-953b6db0b3aa\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612559,\"participants\":[{\"id\":999776,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999777,\"user_id\":null,\"contact_id\":97,\"lead_id\":null}]} {\"correlation_id\":\"6231af67-0852-4f81-a268-953b6db0b3aa\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"6231af67-0852-4f81-a268-953b6db0b3aa\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"6231af67-0852-4f81-a268-953b6db0b3aa\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"6231af67-0852-4f81-a268-953b6db0b3aa\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"6231af67-0852-4f81-a268-953b6db0b3aa\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612559,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"6231af67-0852-4f81-a268-953b6db0b3aa\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: ProspectCache - Searching DB for opportunity by owner {\"account_id\":69,\"contact_id\":97,\"owner_id\":206} {\"correlation_id\":\"6231af67-0852-4f81-a268-953b6db0b3aa\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: ProspectCache - Fallback DB opportunity search {\"account_id\":69,\"contact_id\":97} {\"correlation_id\":\"6231af67-0852-4f81-a268-953b6db0b3aa\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: ProspectCache - Opportunity DB search results {\"account_id\":69,\"contact_id\":97,\"opportunity_id\":5011} {\"correlation_id\":\"6231af67-0852-4f81-a268-953b6db0b3aa\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinsoncrusoe@test.com\"} {\"correlation_id\":\"6231af67-0852-4f81-a268-953b6db0b3aa\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612559,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"6231af67-0852-4f81-a268-953b6db0b3aa\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612559} {\"correlation_id\":\"6231af67-0852-4f81-a268-953b6db0b3aa\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612559,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"6231af67-0852-4f81-a268-953b6db0b3aa\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612559,\"remote_search\":true,\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":5011,\"stage_id\":34} {\"correlation_id\":\"6231af67-0852-4f81-a268-953b6db0b3aa\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612558,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":5011,\"stage_id\":34}} {\"correlation_id\":\"f8c4783d-18f2-45b6-a1b8-e9d80b788104\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612558} {\"correlation_id\":\"f8c4783d-18f2-45b6-a1b8-e9d80b788104\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612558,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"f8c4783d-18f2-45b6-a1b8-e9d80b788104\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612558,\"participants\":[{\"id\":999774,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999775,\"user_id\":null,\"contact_id\":97,\"lead_id\":null}]} {\"correlation_id\":\"f8c4783d-18f2-45b6-a1b8-e9d80b788104\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"f8c4783d-18f2-45b6-a1b8-e9d80b788104\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"f8c4783d-18f2-45b6-a1b8-e9d80b788104\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"f8c4783d-18f2-45b6-a1b8-e9d80b788104\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"f8c4783d-18f2-45b6-a1b8-e9d80b788104\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612558,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"f8c4783d-18f2-45b6-a1b8-e9d80b788104\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinsoncrusoe@test.com\"} {\"correlation_id\":\"f8c4783d-18f2-45b6-a1b8-e9d80b788104\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612558,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"f8c4783d-18f2-45b6-a1b8-e9d80b788104\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612558} {\"correlation_id\":\"f8c4783d-18f2-45b6-a1b8-e9d80b788104\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612558,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"f8c4783d-18f2-45b6-a1b8-e9d80b788104\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612558,\"remote_search\":true,\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":5011,\"stage_id\":34} {\"correlation_id\":\"f8c4783d-18f2-45b6-a1b8-e9d80b788104\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612557,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":5011,\"stage_id\":34}} {\"correlation_id\":\"d087a635-67a6-4fb4-be19-85f309031ebb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612557} {\"correlation_id\":\"d087a635-67a6-4fb4-be19-85f309031ebb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612557,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"d087a635-67a6-4fb4-be19-85f309031ebb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612557,\"participants\":[{\"id\":999772,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999773,\"user_id\":null,\"contact_id\":97,\"lead_id\":null}]} {\"correlation_id\":\"d087a635-67a6-4fb4-be19-85f309031ebb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"d087a635-67a6-4fb4-be19-85f309031ebb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"d087a635-67a6-4fb4-be19-85f309031ebb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d087a635-67a6-4fb4-be19-85f309031ebb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"d087a635-67a6-4fb4-be19-85f309031ebb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612557,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"d087a635-67a6-4fb4-be19-85f309031ebb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinsoncrusoe@test.com\"} {\"correlation_id\":\"d087a635-67a6-4fb4-be19-85f309031ebb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612557,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"d087a635-67a6-4fb4-be19-85f309031ebb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612557} {\"correlation_id\":\"d087a635-67a6-4fb4-be19-85f309031ebb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612557,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"d087a635-67a6-4fb4-be19-85f309031ebb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612557,\"remote_search\":true,\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":5011,\"stage_id\":34} {\"correlation_id\":\"d087a635-67a6-4fb4-be19-85f309031ebb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612556,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":5011,\"stage_id\":34}} {\"correlation_id\":\"462bb4b0-f644-4f69-b4a8-6030370c2202\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612556} {\"correlation_id\":\"462bb4b0-f644-4f69-b4a8-6030370c2202\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612556,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"462bb4b0-f644-4f69-b4a8-6030370c2202\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612556,\"participants\":[{\"id\":999770,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999771,\"user_id\":null,\"contact_id\":97,\"lead_id\":null}]} {\"correlation_id\":\"462bb4b0-f644-4f69-b4a8-6030370c2202\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"462bb4b0-f644-4f69-b4a8-6030370c2202\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"462bb4b0-f644-4f69-b4a8-6030370c2202\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"462bb4b0-f644-4f69-b4a8-6030370c2202\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"462bb4b0-f644-4f69-b4a8-6030370c2202\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612556,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"462bb4b0-f644-4f69-b4a8-6030370c2202\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinsoncrusoe@test.com\"} {\"correlation_id\":\"462bb4b0-f644-4f69-b4a8-6030370c2202\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612556,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"462bb4b0-f644-4f69-b4a8-6030370c2202\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612556} {\"correlation_id\":\"462bb4b0-f644-4f69-b4a8-6030370c2202\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612556,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"462bb4b0-f644-4f69-b4a8-6030370c2202\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612556,\"remote_search\":true,\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":5011,\"stage_id\":34} {\"correlation_id\":\"462bb4b0-f644-4f69-b4a8-6030370c2202\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612555,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":5011,\"stage_id\":34}} {\"correlation_id\":\"988eea33-83d6-41f7-93a3-9737c43ee6a3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612555} {\"correlation_id\":\"988eea33-83d6-41f7-93a3-9737c43ee6a3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [ EsUpdateProcessManager ] Finished updating entities in ES {\"worker\":\"\",\"peak_memory\":\"99.73 MB\",\"elapsed_seconds\":4.12,\"update_target\":\"activities\",\"should_iterate_again\":false} {\"correlation_id\":\"4ad05333-9afb-492e-9f0f-b2909ac45b32\",\"trace_id\":\"3d8feb24-b173-4158-b0a4-4cf33af85066\"}\n[2026-05-11 10:43:33] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612555,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"988eea33-83d6-41f7-93a3-9737c43ee6a3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612555,\"participants\":[{\"id\":999768,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999769,\"user_id\":null,\"contact_id\":97,\"lead_id\":null}]} {\"correlation_id\":\"988eea33-83d6-41f7-93a3-9737c43ee6a3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"988eea33-83d6-41f7-93a3-9737c43ee6a3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"988eea33-83d6-41f7-93a3-9737c43ee6a3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"988eea33-83d6-41f7-93a3-9737c43ee6a3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"988eea33-83d6-41f7-93a3-9737c43ee6a3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612555,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"988eea33-83d6-41f7-93a3-9737c43ee6a3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinsoncrusoe@test.com\"} {\"correlation_id\":\"988eea33-83d6-41f7-93a3-9737c43ee6a3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612555,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"988eea33-83d6-41f7-93a3-9737c43ee6a3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612555} {\"correlation_id\":\"988eea33-83d6-41f7-93a3-9737c43ee6a3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612555,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"988eea33-83d6-41f7-93a3-9737c43ee6a3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612555,\"remote_search\":true,\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":5011,\"stage_id\":34} {\"correlation_id\":\"988eea33-83d6-41f7-93a3-9737c43ee6a3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612554,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":5011,\"stage_id\":34}} {\"correlation_id\":\"7ff43be6-a32a-412c-bb3d-1bd40aa30f65\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612554} {\"correlation_id\":\"7ff43be6-a32a-412c-bb3d-1bd40aa30f65\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612554,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"7ff43be6-a32a-412c-bb3d-1bd40aa30f65\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612554,\"participants\":[{\"id\":999766,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999767,\"user_id\":null,\"contact_id\":97,\"lead_id\":null}]} {\"correlation_id\":\"7ff43be6-a32a-412c-bb3d-1bd40aa30f65\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"7ff43be6-a32a-412c-bb3d-1bd40aa30f65\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"7ff43be6-a32a-412c-bb3d-1bd40aa30f65\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"7ff43be6-a32a-412c-bb3d-1bd40aa30f65\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"7ff43be6-a32a-412c-bb3d-1bd40aa30f65\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612554,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"7ff43be6-a32a-412c-bb3d-1bd40aa30f65\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinsoncrusoe@test.com\"} {\"correlation_id\":\"7ff43be6-a32a-412c-bb3d-1bd40aa30f65\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612554,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"7ff43be6-a32a-412c-bb3d-1bd40aa30f65\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612554} {\"correlation_id\":\"7ff43be6-a32a-412c-bb3d-1bd40aa30f65\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612554,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"7ff43be6-a32a-412c-bb3d-1bd40aa30f65\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612554,\"remote_search\":true,\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":5011,\"stage_id\":34} {\"correlation_id\":\"7ff43be6-a32a-412c-bb3d-1bd40aa30f65\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612553,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":5011,\"stage_id\":34}} {\"correlation_id\":\"3e6b8bf5-c432-4502-a026-aaf235b3b359\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612553} {\"correlation_id\":\"3e6b8bf5-c432-4502-a026-aaf235b3b359\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612553,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"3e6b8bf5-c432-4502-a026-aaf235b3b359\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612553,\"participants\":[{\"id\":999764,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999765,\"user_id\":null,\"contact_id\":97,\"lead_id\":null}]} {\"correlation_id\":\"3e6b8bf5-c432-4502-a026-aaf235b3b359\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"3e6b8bf5-c432-4502-a026-aaf235b3b359\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"3e6b8bf5-c432-4502-a026-aaf235b3b359\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"3e6b8bf5-c432-4502-a026-aaf235b3b359\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"3e6b8bf5-c432-4502-a026-aaf235b3b359\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612553,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"3e6b8bf5-c432-4502-a026-aaf235b3b359\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinsoncrusoe@test.com\"} {\"correlation_id\":\"3e6b8bf5-c432-4502-a026-aaf235b3b359\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612553,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"3e6b8bf5-c432-4502-a026-aaf235b3b359\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612553} {\"correlation_id\":\"3e6b8bf5-c432-4502-a026-aaf235b3b359\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612553,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"3e6b8bf5-c432-4502-a026-aaf235b3b359\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612553,\"remote_search\":true,\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":5011,\"stage_id\":34} {\"correlation_id\":\"3e6b8bf5-c432-4502-a026-aaf235b3b359\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"calendar:sync\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:34] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612552,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":5011,\"stage_id\":34}} {\"correlation_id\":\"1d70d062-f785-4c2c-81be-d992f1ef25c0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612552} {\"correlation_id\":\"1d70d062-f785-4c2c-81be-d992f1ef25c0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:retry-failed\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"c2dd1ea7-a2bd-44ee-95a4-60f88c2fcef7\",\"trace_id\":\"dcda0937-682f-46f0-bdc7-d2054de63c89\"}\n[2026-05-11 10:43:34] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612552,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"1d70d062-f785-4c2c-81be-d992f1ef25c0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612552,\"participants\":[{\"id\":999762,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999763,\"user_id\":null,\"contact_id\":97,\"lead_id\":null}]} {\"correlation_id\":\"1d70d062-f785-4c2c-81be-d992f1ef25c0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.NOTICE: Calendar sync start {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:34] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"1d70d062-f785-4c2c-81be-d992f1ef25c0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"1d70d062-f785-4c2c-81be-d992f1ef25c0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"1d70d062-f785-4c2c-81be-d992f1ef25c0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"1d70d062-f785-4c2c-81be-d992f1ef25c0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612552,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"1d70d062-f785-4c2c-81be-d992f1ef25c0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinsoncrusoe@test.com\"} {\"correlation_id\":\"1d70d062-f785-4c2c-81be-d992f1ef25c0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612552,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"1d70d062-f785-4c2c-81be-d992f1ef25c0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612552} {\"correlation_id\":\"1d70d062-f785-4c2c-81be-d992f1ef25c0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612552,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"1d70d062-f785-4c2c-81be-d992f1ef25c0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612552,\"remote_search\":true,\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":5011,\"stage_id\":34} {\"correlation_id\":\"1d70d062-f785-4c2c-81be-d992f1ef25c0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:retry-failed\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":60.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"c2dd1ea7-a2bd-44ee-95a4-60f88c2fcef7\",\"trace_id\":\"dcda0937-682f-46f0-bdc7-d2054de63c89\"}\n[2026-05-11 10:43:34] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612551,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":5011,\"stage_id\":34}} {\"correlation_id\":\"35f5694e-2521-4089-a5f1-4adc747a40bf\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612551} {\"correlation_id\":\"35f5694e-2521-4089-a5f1-4adc747a40bf\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612551,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"35f5694e-2521-4089-a5f1-4adc747a40bf\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612551,\"participants\":[{\"id\":999760,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999761,\"user_id\":null,\"contact_id\":97,\"lead_id\":null}]} {\"correlation_id\":\"35f5694e-2521-4089-a5f1-4adc747a40bf\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"35f5694e-2521-4089-a5f1-4adc747a40bf\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"35f5694e-2521-4089-a5f1-4adc747a40bf\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"35f5694e-2521-4089-a5f1-4adc747a40bf\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"35f5694e-2521-4089-a5f1-4adc747a40bf\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612551,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"35f5694e-2521-4089-a5f1-4adc747a40bf\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinsoncrusoe@test.com\"} {\"correlation_id\":\"35f5694e-2521-4089-a5f1-4adc747a40bf\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612551,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"35f5694e-2521-4089-a5f1-4adc747a40bf\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612551} {\"correlation_id\":\"35f5694e-2521-4089-a5f1-4adc747a40bf\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612551,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"35f5694e-2521-4089-a5f1-4adc747a40bf\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612551,\"remote_search\":true,\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":5011,\"stage_id\":34} {\"correlation_id\":\"35f5694e-2521-4089-a5f1-4adc747a40bf\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1393,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:34] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1393,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:34] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612550,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":5011,\"stage_id\":34}} {\"correlation_id\":\"046f384b-a938-4138-a9e6-7df0ff6eb90b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:34] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612550} {\"correlation_id\":\"046f384b-a938-4138-a9e6-7df0ff6eb90b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612550,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"046f384b-a938-4138-a9e6-7df0ff6eb90b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1393,\"provider\":\"google\",\"refreshToken\":\"5aa7e2d96b53201cd16fca5d2e4ef3ad03320971fc064781d18aee3ae7b99fbf\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:34] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612550,\"participants\":[{\"id\":999758,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999759,\"user_id\":null,\"contact_id\":97,\"lead_id\":null}]} {\"correlation_id\":\"046f384b-a938-4138-a9e6-7df0ff6eb90b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"046f384b-a938-4138-a9e6-7df0ff6eb90b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"046f384b-a938-4138-a9e6-7df0ff6eb90b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"046f384b-a938-4138-a9e6-7df0ff6eb90b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"046f384b-a938-4138-a9e6-7df0ff6eb90b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612550,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"046f384b-a938-4138-a9e6-7df0ff6eb90b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinsoncrusoe@test.com\"} {\"correlation_id\":\"046f384b-a938-4138-a9e6-7df0ff6eb90b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612550,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"046f384b-a938-4138-a9e6-7df0ff6eb90b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612550} {\"correlation_id\":\"046f384b-a938-4138-a9e6-7df0ff6eb90b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612550,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"046f384b-a938-4138-a9e6-7df0ff6eb90b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612550,\"remote_search\":true,\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":5011,\"stage_id\":34} {\"correlation_id\":\"046f384b-a938-4138-a9e6-7df0ff6eb90b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612549,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":5011,\"stage_id\":34}} {\"correlation_id\":\"1fe4380c-c6ba-40c4-9bdc-65cf0a157cda\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612549} {\"correlation_id\":\"1fe4380c-c6ba-40c4-9bdc-65cf0a157cda\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612549,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"1fe4380c-c6ba-40c4-9bdc-65cf0a157cda\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612549,\"participants\":[{\"id\":999756,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999757,\"user_id\":null,\"contact_id\":97,\"lead_id\":null}]} {\"correlation_id\":\"1fe4380c-c6ba-40c4-9bdc-65cf0a157cda\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1393,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Account has been deleted\"}} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:34] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:34] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1393,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:34] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"1fe4380c-c6ba-40c4-9bdc-65cf0a157cda\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"1fe4380c-c6ba-40c4-9bdc-65cf0a157cda\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"1fe4380c-c6ba-40c4-9bdc-65cf0a157cda\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"1fe4380c-c6ba-40c4-9bdc-65cf0a157cda\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1387,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:34] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1387,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:34] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:34] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612549,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"1fe4380c-c6ba-40c4-9bdc-65cf0a157cda\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1387,\"provider\":\"google\",\"refreshToken\":\"8157ac6de94842937194009e9c50e459253600f799dacf6a40755ffdbeb5bba6\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:34] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinsoncrusoe@test.com\"} {\"correlation_id\":\"1fe4380c-c6ba-40c4-9bdc-65cf0a157cda\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612549,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"1fe4380c-c6ba-40c4-9bdc-65cf0a157cda\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612549} {\"correlation_id\":\"1fe4380c-c6ba-40c4-9bdc-65cf0a157cda\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612549,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"1fe4380c-c6ba-40c4-9bdc-65cf0a157cda\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612549,\"remote_search\":true,\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":5011,\"stage_id\":34} {\"correlation_id\":\"1fe4380c-c6ba-40c4-9bdc-65cf0a157cda\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612365,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"9f080e5f-9e25-468c-9d04-5cdf618dc528\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612365} {\"correlation_id\":\"9f080e5f-9e25-468c-9d04-5cdf618dc528\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612365,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"9f080e5f-9e25-468c-9d04-5cdf618dc528\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612365,\"participants\":[{\"id\":999563,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null},{\"id\":999564,\"user_id\":206,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"9f080e5f-9e25-468c-9d04-5cdf618dc528\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"9f080e5f-9e25-468c-9d04-5cdf618dc528\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"9f080e5f-9e25-468c-9d04-5cdf618dc528\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"9f080e5f-9e25-468c-9d04-5cdf618dc528\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1387,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Account has been deleted\"}} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"9f080e5f-9e25-468c-9d04-5cdf618dc528\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1387,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"9f080e5f-9e25-468c-9d04-5cdf618dc528\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612365,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"9f080e5f-9e25-468c-9d04-5cdf618dc528\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1348,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612365,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"9f080e5f-9e25-468c-9d04-5cdf618dc528\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1348,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612365} {\"correlation_id\":\"9f080e5f-9e25-468c-9d04-5cdf618dc528\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612365,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"9f080e5f-9e25-468c-9d04-5cdf618dc528\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1348,\"provider\":\"google\",\"refreshToken\":\"9e7d13d3032d0cb1b79d8e95aef01383e8e91eb52ff8ee960c8a0b6b95cd8c73\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612365,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"9f080e5f-9e25-468c-9d04-5cdf618dc528\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612360,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"e1af9830-ccba-44db-a1a0-d9ac3ec0da4d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612360} {\"correlation_id\":\"e1af9830-ccba-44db-a1a0-d9ac3ec0da4d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612360,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"e1af9830-ccba-44db-a1a0-d9ac3ec0da4d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612360,\"participants\":[{\"id\":999552,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null},{\"id\":999553,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999565,\"user_id\":null,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"e1af9830-ccba-44db-a1a0-d9ac3ec0da4d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"e1af9830-ccba-44db-a1a0-d9ac3ec0da4d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"e1af9830-ccba-44db-a1a0-d9ac3ec0da4d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"e1af9830-ccba-44db-a1a0-d9ac3ec0da4d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"e1af9830-ccba-44db-a1a0-d9ac3ec0da4d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"e1af9830-ccba-44db-a1a0-d9ac3ec0da4d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612360,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"e1af9830-ccba-44db-a1a0-d9ac3ec0da4d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1348,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Bad Request\"}} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1348,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1361,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1361,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1361,\"provider\":\"google\",\"refreshToken\":\"6c843da199c2b9907445329304fcc4ec5057a4ee748d8299641764395c08e1fd\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1361,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Account has been deleted\"}} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1361,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [Hubspot] Pagination completed {\"team_id\":2,\"endpoint\":\"https://api.hubapi.com/crm/v3/objects/contact/search\",\"total_requests\":1,\"total_records_fetched\":0,\"total_elapsed_seconds\":0.24,\"average_seconds_per_request\":0.24} {\"correlation_id\":\"e1af9830-ccba-44db-a1a0-d9ac3ec0da4d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612360,\"participants_processed\":3,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"e1af9830-ccba-44db-a1a0-d9ac3ec0da4d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1310,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612360} {\"correlation_id\":\"e1af9830-ccba-44db-a1a0-d9ac3ec0da4d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1310,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612360,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"e1af9830-ccba-44db-a1a0-d9ac3ec0da4d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1310,\"provider\":\"google\",\"refreshToken\":\"e34818922c2830a660813a63f6169a4a9a992ae2cccd7dc8dd7796cfdb470ef1\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612360,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"e1af9830-ccba-44db-a1a0-d9ac3ec0da4d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612340,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"eb3baeb0-9f99-4e3d-98f1-fdc0d69023fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612340} {\"correlation_id\":\"eb3baeb0-9f99-4e3d-98f1-fdc0d69023fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612340,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"eb3baeb0-9f99-4e3d-98f1-fdc0d69023fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612340,\"participants\":[{\"id\":999516,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null},{\"id\":999517,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999518,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":999519,\"user_id\":null,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"eb3baeb0-9f99-4e3d-98f1-fdc0d69023fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"eb3baeb0-9f99-4e3d-98f1-fdc0d69023fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"eb3baeb0-9f99-4e3d-98f1-fdc0d69023fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"eb3baeb0-9f99-4e3d-98f1-fdc0d69023fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"eb3baeb0-9f99-4e3d-98f1-fdc0d69023fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"eb3baeb0-9f99-4e3d-98f1-fdc0d69023fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612340,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"eb3baeb0-9f99-4e3d-98f1-fdc0d69023fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1310,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Bad Request\"}} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1310,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1333,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1333,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1333,\"provider\":\"google\",\"refreshToken\":\"6c902986546d8e8da1dc539b046cdc1d458f519acc972e5b5f1d6a1a295165e0\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [Hubspot] Pagination completed {\"team_id\":2,\"endpoint\":\"https://api.hubapi.com/crm/v3/objects/contact/search\",\"total_requests\":1,\"total_records_fetched\":0,\"total_elapsed_seconds\":0.22,\"average_seconds_per_request\":0.22} {\"correlation_id\":\"eb3baeb0-9f99-4e3d-98f1-fdc0d69023fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612340,\"participants_processed\":4,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"eb3baeb0-9f99-4e3d-98f1-fdc0d69023fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612340} {\"correlation_id\":\"eb3baeb0-9f99-4e3d-98f1-fdc0d69023fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612340,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"eb3baeb0-9f99-4e3d-98f1-fdc0d69023fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612340,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"eb3baeb0-9f99-4e3d-98f1-fdc0d69023fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612339,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"68ce2e50-cf66-4aab-9ce0-ab242e6027c4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612339} {\"correlation_id\":\"68ce2e50-cf66-4aab-9ce0-ab242e6027c4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612339,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"68ce2e50-cf66-4aab-9ce0-ab242e6027c4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612339,\"participants\":[{\"id\":999514,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null},{\"id\":999515,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999540,\"user_id\":null,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"68ce2e50-cf66-4aab-9ce0-ab242e6027c4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"68ce2e50-cf66-4aab-9ce0-ab242e6027c4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"68ce2e50-cf66-4aab-9ce0-ab242e6027c4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"68ce2e50-cf66-4aab-9ce0-ab242e6027c4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"68ce2e50-cf66-4aab-9ce0-ab242e6027c4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"68ce2e50-cf66-4aab-9ce0-ab242e6027c4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612339,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"68ce2e50-cf66-4aab-9ce0-ab242e6027c4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612339,\"participants_processed\":3,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"68ce2e50-cf66-4aab-9ce0-ab242e6027c4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1333,\"provider\":\"google\",\"responseBody\":{\"error\":\"unauthorized_client\",\"error_description\":\"Unauthorized\"}} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612339} {\"correlation_id\":\"68ce2e50-cf66-4aab-9ce0-ab242e6027c4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1333,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612339,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"68ce2e50-cf66-4aab-9ce0-ab242e6027c4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612339,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"68ce2e50-cf66-4aab-9ce0-ab242e6027c4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1368,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1368,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1368,\"provider\":\"google\",\"refreshToken\":\"d2f128898ff8543bd16b69cfae37896ab85119b0f5ed2b431d739593bb600333\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612336,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"ffc0ea4a-744d-49a4-a9ad-736fbda03384\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612336} {\"correlation_id\":\"ffc0ea4a-744d-49a4-a9ad-736fbda03384\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612336,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"ffc0ea4a-744d-49a4-a9ad-736fbda03384\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612336,\"participants\":[{\"id\":999508,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null},{\"id\":999509,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999512,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":999513,\"user_id\":null,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"ffc0ea4a-744d-49a4-a9ad-736fbda03384\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"ffc0ea4a-744d-49a4-a9ad-736fbda03384\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"ffc0ea4a-744d-49a4-a9ad-736fbda03384\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"ffc0ea4a-744d-49a4-a9ad-736fbda03384\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"ffc0ea4a-744d-49a4-a9ad-736fbda03384\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"ffc0ea4a-744d-49a4-a9ad-736fbda03384\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612336,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"ffc0ea4a-744d-49a4-a9ad-736fbda03384\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612336,\"participants_processed\":4,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"ffc0ea4a-744d-49a4-a9ad-736fbda03384\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612336} {\"correlation_id\":\"ffc0ea4a-744d-49a4-a9ad-736fbda03384\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612336,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"ffc0ea4a-744d-49a4-a9ad-736fbda03384\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612336,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"ffc0ea4a-744d-49a4-a9ad-736fbda03384\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612183,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36}} {\"correlation_id\":\"48880be8-e032-41b2-948e-c4ec99247548\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612183} {\"correlation_id\":\"48880be8-e032-41b2-948e-c4ec99247548\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612183,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"48880be8-e032-41b2-948e-c4ec99247548\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612183,\"participants\":[{\"id\":999227,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null},{\"id\":999228,\"user_id\":261,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"48880be8-e032-41b2-948e-c4ec99247548\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"48880be8-e032-41b2-948e-c4ec99247548\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"48880be8-e032-41b2-948e-c4ec99247548\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"48880be8-e032-41b2-948e-c4ec99247548\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"48880be8-e032-41b2-948e-c4ec99247548\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"48880be8-e032-41b2-948e-c4ec99247548\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612183,\"team_id\":2,\"email\":\"adelina.petrova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"48880be8-e032-41b2-948e-c4ec99247548\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612183,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"48880be8-e032-41b2-948e-c4ec99247548\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612183} {\"correlation_id\":\"48880be8-e032-41b2-948e-c4ec99247548\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612183,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"48880be8-e032-41b2-948e-c4ec99247548\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612183,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36} {\"correlation_id\":\"48880be8-e032-41b2-948e-c4ec99247548\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612182,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36}} {\"correlation_id\":\"3858f13d-426c-474e-98cf-11576b98e5dd\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612182} {\"correlation_id\":\"3858f13d-426c-474e-98cf-11576b98e5dd\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612182,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"3858f13d-426c-474e-98cf-11576b98e5dd\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612182,\"participants\":[{\"id\":999225,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null},{\"id\":999226,\"user_id\":261,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"3858f13d-426c-474e-98cf-11576b98e5dd\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"3858f13d-426c-474e-98cf-11576b98e5dd\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"3858f13d-426c-474e-98cf-11576b98e5dd\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"3858f13d-426c-474e-98cf-11576b98e5dd\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"3858f13d-426c-474e-98cf-11576b98e5dd\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"3858f13d-426c-474e-98cf-11576b98e5dd\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612182,\"team_id\":2,\"email\":\"adelina.petrova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"3858f13d-426c-474e-98cf-11576b98e5dd\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612182,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"3858f13d-426c-474e-98cf-11576b98e5dd\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612182} {\"correlation_id\":\"3858f13d-426c-474e-98cf-11576b98e5dd\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612182,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"3858f13d-426c-474e-98cf-11576b98e5dd\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612182,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36} {\"correlation_id\":\"3858f13d-426c-474e-98cf-11576b98e5dd\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612181,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36}} {\"correlation_id\":\"33fd8677-950a-4069-9642-511743b8b619\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612181} {\"correlation_id\":\"33fd8677-950a-4069-9642-511743b8b619\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612181,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"33fd8677-950a-4069-9642-511743b8b619\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612181,\"participants\":[{\"id\":999223,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null},{\"id\":999224,\"user_id\":261,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"33fd8677-950a-4069-9642-511743b8b619\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"33fd8677-950a-4069-9642-511743b8b619\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"33fd8677-950a-4069-9642-511743b8b619\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"33fd8677-950a-4069-9642-511743b8b619\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"33fd8677-950a-4069-9642-511743b8b619\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"33fd8677-950a-4069-9642-511743b8b619\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1368,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Bad Request\"}} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612181,\"team_id\":2,\"email\":\"adelina.petrova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"33fd8677-950a-4069-9642-511743b8b619\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1368,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612181,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"33fd8677-950a-4069-9642-511743b8b619\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612181} {\"correlation_id\":\"33fd8677-950a-4069-9642-511743b8b619\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612181,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"33fd8677-950a-4069-9642-511743b8b619\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612181,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36} {\"correlation_id\":\"33fd8677-950a-4069-9642-511743b8b619\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1365,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1365,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1365,\"provider\":\"google\",\"refreshToken\":\"7676e4a9afcd082b413248ab5ec6e487021fec6a9bdf315860a59cefad9caad8\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612180,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36}} {\"correlation_id\":\"dc184f1d-d7e2-4bc1-92a0-c572af69ba86\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612180} {\"correlation_id\":\"dc184f1d-d7e2-4bc1-92a0-c572af69ba86\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612180,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"dc184f1d-d7e2-4bc1-92a0-c572af69ba86\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612180,\"participants\":[{\"id\":999221,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null},{\"id\":999222,\"user_id\":261,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"dc184f1d-d7e2-4bc1-92a0-c572af69ba86\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"dc184f1d-d7e2-4bc1-92a0-c572af69ba86\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"dc184f1d-d7e2-4bc1-92a0-c572af69ba86\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"dc184f1d-d7e2-4bc1-92a0-c572af69ba86\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"dc184f1d-d7e2-4bc1-92a0-c572af69ba86\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"dc184f1d-d7e2-4bc1-92a0-c572af69ba86\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612180,\"team_id\":2,\"email\":\"adelina.petrova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"dc184f1d-d7e2-4bc1-92a0-c572af69ba86\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612180,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"dc184f1d-d7e2-4bc1-92a0-c572af69ba86\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612180} {\"correlation_id\":\"dc184f1d-d7e2-4bc1-92a0-c572af69ba86\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612180,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"dc184f1d-d7e2-4bc1-92a0-c572af69ba86\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612180,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36} {\"correlation_id\":\"dc184f1d-d7e2-4bc1-92a0-c572af69ba86\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":611455,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"8f14fe33-00cc-4910-996e-efa541fc5bb7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":611455} {\"correlation_id\":\"8f14fe33-00cc-4910-996e-efa541fc5bb7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":611455,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"8f14fe33-00cc-4910-996e-efa541fc5bb7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":611455,\"participants\":[{\"id\":997961,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":997962,\"user_id\":1460,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"8f14fe33-00cc-4910-996e-efa541fc5bb7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"8f14fe33-00cc-4910-996e-efa541fc5bb7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"8f14fe33-00cc-4910-996e-efa541fc5bb7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"8f14fe33-00cc-4910-996e-efa541fc5bb7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"8f14fe33-00cc-4910-996e-efa541fc5bb7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [Prospect match] Cache miss, calling the API {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"8f14fe33-00cc-4910-996e-efa541fc5bb7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1365,\"provider\":\"google\",\"responseBody\":{\"error\":\"unauthorized_client\",\"error_description\":\"Unauthorized\"}} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1365,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1364,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1364,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1364,\"provider\":\"google\",\"refreshToken\":\"dd5882ebce76e645292ce33ae74238abbb77c0a4ecc6a2bfe723cad82e72ba8e\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.INFO: [Hubspot] Failed to fetch contact {\"email\":\"support@staging.jiminny.com\",\"reason\":\"[404] Client error: `GET https://api.hubapi.com/crm/v3/objects/contacts/support%40staging.jiminny.com?properties=email%2Cfirstname%2Clastname%2Ccountry%2Cphone%2Cmobilephone%2Cjobtitle%2Chubspot_owner_id%2Cassociatedcompanyid%2Cphoto&archived=0&idProperty=email` resulted in a `404 Not Found` response\"} {\"correlation_id\":\"8f14fe33-00cc-4910-996e-efa541fc5bb7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [Prospect match] API returned empty result, caching the miss with empty prospect data {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"8f14fe33-00cc-4910-996e-efa541fc5bb7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1364,\"provider\":\"google\",\"responseBody\":{\"error\":\"unauthorized_client\",\"error_description\":\"Unauthorized\"}} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1364,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1370,\"provider\":\"office\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1370,\"provider\":\"office\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1370,\"provider\":\"office\",\"refreshToken\":\"b7ee8035306d0043cea6e00e7c4fe14f745e44074a1194db62a31cdf8b70af3e\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.INFO: [Hubspot] Pagination completed {\"team_id\":2,\"endpoint\":\"https://api.hubapi.com/crm/v3/objects/contact/search\",\"total_requests\":1,\"total_records_fetched\":0,\"total_elapsed_seconds\":0.21,\"average_seconds_per_request\":0.21} {\"correlation_id\":\"8f14fe33-00cc-4910-996e-efa541fc5bb7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"8f14fe33-00cc-4910-996e-efa541fc5bb7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [Prospect match] Cache miss {\"identifier_type\":\"domain\",\"identifier\":\"jiminny.com\",\"crm\":\"hubspot\"} {\"correlation_id\":\"8f14fe33-00cc-4910-996e-efa541fc5bb7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [Prospect match] Cache miss, calling the API {\"identifier_type\":\"domain\",\"identifier\":\"jiminny.com\"} {\"correlation_id\":\"8f14fe33-00cc-4910-996e-efa541fc5bb7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1370,\"provider\":\"office\",\"responseBody\":\"{\\\"error\\\":\\\"invalid_client\\\",\\\"error_description\\\":\\\"AADSTS7000215: Invalid client secret provided. Ensure the secret being sent in the request is the client secret value, not the client secret ID, for a secret added to app 'bbcbb2ef-6200-4fae-82bd-d81f5dd738da'. Trace ID: 329b429c-bad8-4f19-bace-40e48a111900 Correlation ID: 4f12c9af-e118-468c-8441-ebc325071d4b Timestamp: 2026-05-11 10:43:36Z\\\",\\\"error_codes\\\":[7000215],\\\"timestamp\\\":\\\"2026-05-11 10:43:36Z\\\",\\\"trace_id\\\":\\\"329b429c-bad8-4f19-bace-40e48a111900\\\",\\\"correlation_id\\\":\\\"4f12c9af-e118-468c-8441-ebc325071d4b\\\",\\\"error_uri\\\":\\\"https://login.microsoftonline.com/error?code=7000215\\\"}\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1370,\"provider\":\"office\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1202,\"provider\":\"office\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1202,\"provider\":\"office\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1202,\"provider\":\"office\",\"refreshToken\":\"b458799ccc29b21a6e2eb5260fdb63e49ccba21bf942a3973fb63799bd7f0afe\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.INFO: [HubSpot] importAccount {\"crm_provider_id\":\"749766179\",\"config_id\":2} {\"correlation_id\":\"8f14fe33-00cc-4910-996e-efa541fc5bb7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [HubSpot] CRM Search requested {\"request\":{\"filterGroups\":[{\"filters\":[{\"propertyName\":\"associations.company\",\"operator\":\"EQ\",\"value\":\"749766179\"},{\"propertyName\":\"dealstage\",\"operator\":\"NOT_IN\",\"values\":[\"closedwon\",\"4040964\",\"59247967\"]},{\"propertyName\":\"dealstage\",\"operator\":\"NOT_IN\",\"values\":[\"closedlost\",\"4040965\",\"59247968\"]}]}],\"sorts\":[{\"propertyName\":\"modifieddate\",\"direction\":\"DESCENDING\"}],\"properties\":[\"dealname\",\"amount\",\"hubspot_owner_id\",\"pipeline\",\"dealstage\",\"closedate\",\"deal_currency_code\"],\"limit\":200}} {\"correlation_id\":\"8f14fe33-00cc-4910-996e-efa541fc5bb7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:37] local.INFO: [Hubspot] Pagination completed {\"team_id\":2,\"endpoint\":\"https://api.hubapi.com/crm/v3/objects/deals/search\",\"total_requests\":1,\"total_records_fetched\":10,\"total_elapsed_seconds\":0.24,\"average_seconds_per_request\":0.24} {\"correlation_id\":\"8f14fe33-00cc-4910-996e-efa541fc5bb7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:37] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":611455,\"team_id\":2,\"email\":\"aneliya.angelova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"8f14fe33-00cc-4910-996e-efa541fc5bb7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:37] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":611455,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"8f14fe33-00cc-4910-996e-efa541fc5bb7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:37] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":611455} {\"correlation_id\":\"8f14fe33-00cc-4910-996e-efa541fc5bb7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:37] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":611455,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"8f14fe33-00cc-4910-996e-efa541fc5bb7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:37] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":611455,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"8f14fe33-00cc-4910-996e-efa541fc5bb7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:37] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":611451,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"7ea49a4a-ec21-4b93-b990-d1e79531b72a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:37] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":611451} {\"correlation_id\":\"7ea49a4a-ec21-4b93-b990-d1e79531b72a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:37] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":611451,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"7ea49a4a-ec21-4b93-b990-d1e79531b72a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:37] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":611451,\"participants\":[{\"id\":997955,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":997956,\"user_id\":18,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"7ea49a4a-ec21-4b93-b990-d1e79531b72a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:37] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"7ea49a4a-ec21-4b93-b990-d1e79531b72a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:37] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"7ea49a4a-ec21-4b93-b990-d1e79531b72a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:37] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"7ea49a4a-ec21-4b93-b990-d1e79531b72a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:37] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"7ea49a4a-ec21-4b93-b990-d1e79531b72a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:37] local.INFO: [Prospect match] Cache miss, calling the API {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"7ea49a4a-ec21-4b93-b990-d1e79531b72a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:38] local.INFO: [Hubspot] Failed to fetch contact {\"email\":\"support@staging.jiminny.com\",\"reason\":\"[404] Client error: `GET https://api.hubapi.com/crm/v3/objects/contacts/support%40staging.jiminny.com?properties=email%2Cfirstname%2Clastname%2Ccountry%2Cphone%2Cmobilephone%2Cjobtitle%2Chubspot_owner_id%2Cassociatedcompanyid%2Cphoto&archived=0&idProperty=email` resulted in a `404 Not Found` response\"} {\"correlation_id\":\"7ea49a4a-ec21-4b93-b990-d1e79531b72a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:38] local.INFO: [Prospect match] API returned empty result, caching the miss with empty prospect data {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"7ea49a4a-ec21-4b93-b990-d1e79531b72a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:38] local.INFO: [Hubspot] Pagination completed {\"team_id\":2,\"endpoint\":\"https://api.hubapi.com/crm/v3/objects/contact/search\",\"total_requests\":1,\"total_records_fetched\":0,\"total_elapsed_seconds\":0.22,\"average_seconds_per_request\":0.22} {\"correlation_id\":\"7ea49a4a-ec21-4b93-b990-d1e79531b72a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:38] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"7ea49a4a-ec21-4b93-b990-d1e79531b72a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:38] local.INFO: [Prospect match] Cache miss {\"identifier_type\":\"domain\",\"identifier\":\"jiminny.com\",\"crm\":\"hubspot\"} {\"correlation_id\":\"7ea49a4a-ec21-4b93-b990-d1e79531b72a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:38] local.INFO: [Prospect match] Cache miss, calling the API {\"identifier_type\":\"domain\",\"identifier\":\"jiminny.com\"} {\"correlation_id\":\"7ea49a4a-ec21-4b93-b990-d1e79531b72a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:38] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1202,\"provider\":\"office\",\"responseBody\":\"{\\\"error\\\":\\\"invalid_client\\\",\\\"error_description\\\":\\\"AADSTS7000215: Invalid client secret provided. Ensure the secret being sent in the request is the client secret value, not the client secret ID, for a secret added to app 'bbcbb2ef-6200-4fae-82bd-d81f5dd738da'. Trace ID: e8a4c6ea-4ab7-4915-b7a3-93868da51100 Correlation ID: e670afc0-d33c-4205-924c-76bda5c3d01d Timestamp: 2026-05-11 10:43:38Z\\\",\\\"error_codes\\\":[7000215],\\\"timestamp\\\":\\\"2026-05-11 10:43:38Z\\\",\\\"trace_id\\\":\\\"e8a4c6ea-4ab7-4915-b7a3-93868da51100\\\",\\\"correlation_id\\\":\\\"e670afc0-d33c-4205-924c-76bda5c3d01d\\\",\\\"error_uri\\\":\\\"https://login.microsoftonline.com/error?code=7000215\\\"}\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:38] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:38] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1202,\"provider\":\"office\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:38] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1502,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:38] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1502,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:38] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:38] local.INFO: Calendar sync job dispatched {\"calendar_id\":501} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:38] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1300,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:38] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1300,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:38] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:38] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1300,\"provider\":\"google\",\"refreshToken\":\"4b811db0725fd9602a95943519a7da935e2a5065da7d9ebfcb170752e3e1ddb8\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:38] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1300,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Account has been deleted\"}} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:38] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:38] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1300,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:38] local.INFO: [HubSpot] importAccount {\"crm_provider_id\":\"749766179\",\"config_id\":2} {\"correlation_id\":\"7ea49a4a-ec21-4b93-b990-d1e79531b72a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:38] local.INFO: [HubSpot] CRM Search requested {\"request\":{\"filterGroups\":[{\"filters\":[{\"propertyName\":\"associations.company\",\"operator\":\"EQ\",\"value\":\"749766179\"},{\"propertyName\":\"dealstage\",\"operator\":\"NOT_IN\",\"values\":[\"closedwon\",\"4040964\",\"59247967\"]},{\"propertyName\":\"dealstage\",\"operator\":\"NOT_IN\",\"values\":[\"closedlost\",\"4040965\",\"59247968\"]}]}],\"sorts\":[{\"propertyName\":\"modifieddate\",\"direction\":\"DESCENDING\"}],\"properties\":[\"dealname\",\"amount\",\"hubspot_owner_id\",\"pipeline\",\"dealstage\",\"closedate\",\"deal_currency_code\"],\"limit\":200}} {\"correlation_id\":\"7ea49a4a-ec21-4b93-b990-d1e79531b72a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:38] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1409,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:38] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1409,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:38] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:38] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1409,\"provider\":\"google\",\"refreshToken\":\"e2a3f2d06894894eed1ee87d9db1ace77d4d42ee6e1288a8940ad2c10333b0c4\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:38] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1409,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Bad Request\"}} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:38] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:38] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1409,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:39] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1352,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:39] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1352,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:39] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:39] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1352,\"provider\":\"google\",\"refreshToken\":\"dd4b16b00fdc1216da6b717c02338c073636e29162826b2de6db3f064fc029eb\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:39] local.INFO: [Hubspot] Pagination completed {\"team_id\":2,\"endpoint\":\"https://api.hubapi.com/crm/v3/objects/deals/search\",\"total_requests\":1,\"total_records_fetched\":10,\"total_elapsed_seconds\":0.28,\"average_seconds_per_request\":0.28} {\"correlation_id\":\"7ea49a4a-ec21-4b93-b990-d1e79531b72a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1352,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Token has been expired or revoked.\"}} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:39] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:39] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1352,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:39] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1296,\"provider\":\"office\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:39] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1296,\"provider\":\"office\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:39] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:39] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1296,\"provider\":\"office\",\"refreshToken\":\"011ae723c9d800c674e0b4be76f49fc046dac7d501b66c59ef0d9549cfa56ae5\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:39] local.INFO: [ EsUpdateProcessManager ] Finished updating entities in ES {\"worker\":\"\",\"peak_memory\":\"99.73 MB\",\"elapsed_seconds\":0.9,\"update_target\":\"activities\",\"should_iterate_again\":false} {\"correlation_id\":\"4ad05333-9afb-492e-9f0f-b2909ac45b32\",\"trace_id\":\"3d8feb24-b173-4158-b0a4-4cf33af85066\"}\n[2026-05-11 10:43:39] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":611451,\"team_id\":2,\"email\":\"veselin.kulov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"7ea49a4a-ec21-4b93-b990-d1e79531b72a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":611451,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"7ea49a4a-ec21-4b93-b990-d1e79531b72a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":611451} {\"correlation_id\":\"7ea49a4a-ec21-4b93-b990-d1e79531b72a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":611451,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"7ea49a4a-ec21-4b93-b990-d1e79531b72a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":611451,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"7ea49a4a-ec21-4b93-b990-d1e79531b72a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":611087,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"02dbf6fa-a774-4090-af49-cabec99363d4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":611087} {\"correlation_id\":\"02dbf6fa-a774-4090-af49-cabec99363d4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":611087,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"02dbf6fa-a774-4090-af49-cabec99363d4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":611087,\"participants\":[{\"id\":997368,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":997369,\"user_id\":18,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"02dbf6fa-a774-4090-af49-cabec99363d4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"02dbf6fa-a774-4090-af49-cabec99363d4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"02dbf6fa-a774-4090-af49-cabec99363d4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"02dbf6fa-a774-4090-af49-cabec99363d4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"02dbf6fa-a774-4090-af49-cabec99363d4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"02dbf6fa-a774-4090-af49-cabec99363d4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"02dbf6fa-a774-4090-af49-cabec99363d4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"02dbf6fa-a774-4090-af49-cabec99363d4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":611087,\"team_id\":2,\"email\":\"veselin.kulov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"02dbf6fa-a774-4090-af49-cabec99363d4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":611087,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"02dbf6fa-a774-4090-af49-cabec99363d4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":611087} {\"correlation_id\":\"02dbf6fa-a774-4090-af49-cabec99363d4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":611087,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"02dbf6fa-a774-4090-af49-cabec99363d4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":611087,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"02dbf6fa-a774-4090-af49-cabec99363d4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1296,\"provider\":\"office\",\"responseBody\":\"{\\\"error\\\":\\\"invalid_client\\\",\\\"error_description\\\":\\\"AADSTS7000215: Invalid client secret provided. Ensure the secret being sent in the request is the client secret value, not the client secret ID, for a secret added to app 'bbcbb2ef-6200-4fae-82bd-d81f5dd738da'. Trace ID: 4ff43bd2-d427-4049-964c-57fbcad91a00 Correlation ID: 6e92626f-a36d-4c41-ae7c-3b7296df0110 Timestamp: 2026-05-11 10:43:39Z\\\",\\\"error_codes\\\":[7000215],\\\"timestamp\\\":\\\"2026-05-11 10:43:39Z\\\",\\\"trace_id\\\":\\\"4ff43bd2-d427-4049-964c-57fbcad91a00\\\",\\\"correlation_id\\\":\\\"6e92626f-a36d-4c41-ae7c-3b7296df0110\\\",\\\"error_uri\\\":\\\"https://login.microsoftonline.com/error?code=7000215\\\"}\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:39] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:39] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1296,\"provider\":\"office\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:39] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":611076,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"ae1d456f-4b46-4c79-bb7f-bc2e95d48c9f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":611076} {\"correlation_id\":\"ae1d456f-4b46-4c79-bb7f-bc2e95d48c9f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":611076,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"ae1d456f-4b46-4c79-bb7f-bc2e95d48c9f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":611076,\"participants\":[{\"id\":997346,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":997347,\"user_id\":18,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"ae1d456f-4b46-4c79-bb7f-bc2e95d48c9f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":391,\"provider\":\"office\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:39] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":391,\"provider\":\"office\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:39] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:39] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":391,\"provider\":\"office\",\"refreshToken\":\"00045eebae0f39b34887c6d53f92ae78064f7145e1f4b67754aebd03cfb2d881\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:39] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"ae1d456f-4b46-4c79-bb7f-bc2e95d48c9f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"ae1d456f-4b46-4c79-bb7f-bc2e95d48c9f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"ae1d456f-4b46-4c79-bb7f-bc2e95d48c9f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"ae1d456f-4b46-4c79-bb7f-bc2e95d48c9f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"ae1d456f-4b46-4c79-bb7f-bc2e95d48c9f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"ae1d456f-4b46-4c79-bb7f-bc2e95d48c9f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"ae1d456f-4b46-4c79-bb7f-bc2e95d48c9f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":611076,\"team_id\":2,\"email\":\"veselin.kulov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"ae1d456f-4b46-4c79-bb7f-bc2e95d48c9f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":611076,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"ae1d456f-4b46-4c79-bb7f-bc2e95d48c9f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":611076} {\"correlation_id\":\"ae1d456f-4b46-4c79-bb7f-bc2e95d48c9f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":611076,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"ae1d456f-4b46-4c79-bb7f-bc2e95d48c9f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":611076,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"ae1d456f-4b46-4c79-bb7f-bc2e95d48c9f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1502,\"provider\":\"google\"} {\"correlation_id\":\"0fdd7dce-04e2-4696-ba87-8089a739c81c\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1502,\"provider\":\"google\"} {\"correlation_id\":\"0fdd7dce-04e2-4696-ba87-8089a739c81c\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:40] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"0fdd7dce-04e2-4696-ba87-8089a739c81c\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:40] local.INFO: [Calendar] Processing sync {\"calendarId\":\"a33076c1-8d97-431a-99f0-85c9524e118b\",\"from\":null,\"to\":null,\"delta\":\"CIiFh8TP44kDEIiFh8TP44kDGAUgkZvkzgIokZvkzgI=\",\"last_sync\":\"2024-12-09 07:12:53\",\"dateMode\":\"daily\"} {\"correlation_id\":\"0fdd7dce-04e2-4696-ba87-8089a739c81c\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:40] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"integration-app\",\"crm_owner\":1695,\"team_id\":3143} {\"correlation_id\":\"0fdd7dce-04e2-4696-ba87-8089a739c81c\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1502,\"provider\":\"google\"} {\"correlation_id\":\"0fdd7dce-04e2-4696-ba87-8089a739c81c\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1502,\"provider\":\"google\"} {\"correlation_id\":\"0fdd7dce-04e2-4696-ba87-8089a739c81c\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:40] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"0fdd7dce-04e2-4696-ba87-8089a739c81c\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":610935,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"9f5579a5-e89f-4074-9843-d5c1f123878d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610935} {\"correlation_id\":\"9f5579a5-e89f-4074-9843-d5c1f123878d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610935,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"9f5579a5-e89f-4074-9843-d5c1f123878d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":610935,\"participants\":[{\"id\":997141,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":997142,\"user_id\":18,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"9f5579a5-e89f-4074-9843-d5c1f123878d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"9f5579a5-e89f-4074-9843-d5c1f123878d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"9f5579a5-e89f-4074-9843-d5c1f123878d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"9f5579a5-e89f-4074-9843-d5c1f123878d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"9f5579a5-e89f-4074-9843-d5c1f123878d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"9f5579a5-e89f-4074-9843-d5c1f123878d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"9f5579a5-e89f-4074-9843-d5c1f123878d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"9f5579a5-e89f-4074-9843-d5c1f123878d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":610935,\"team_id\":2,\"email\":\"veselin.kulov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"9f5579a5-e89f-4074-9843-d5c1f123878d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":610935,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"9f5579a5-e89f-4074-9843-d5c1f123878d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610935} {\"correlation_id\":\"9f5579a5-e89f-4074-9843-d5c1f123878d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610935,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"9f5579a5-e89f-4074-9843-d5c1f123878d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":610935,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"9f5579a5-e89f-4074-9843-d5c1f123878d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":610915,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"ee1e1cdc-f0b6-4353-b761-ada15282a954\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610915} {\"correlation_id\":\"ee1e1cdc-f0b6-4353-b761-ada15282a954\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610915,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"ee1e1cdc-f0b6-4353-b761-ada15282a954\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":610915,\"participants\":[{\"id\":997104,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":997105,\"user_id\":18,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"ee1e1cdc-f0b6-4353-b761-ada15282a954\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"ee1e1cdc-f0b6-4353-b761-ada15282a954\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"ee1e1cdc-f0b6-4353-b761-ada15282a954\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"ee1e1cdc-f0b6-4353-b761-ada15282a954\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"ee1e1cdc-f0b6-4353-b761-ada15282a954\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"ee1e1cdc-f0b6-4353-b761-ada15282a954\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"ee1e1cdc-f0b6-4353-b761-ada15282a954\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"ee1e1cdc-f0b6-4353-b761-ada15282a954\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":610915,\"team_id\":2,\"email\":\"veselin.kulov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"ee1e1cdc-f0b6-4353-b761-ada15282a954\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":610915,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"ee1e1cdc-f0b6-4353-b761-ada15282a954\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610915} {\"correlation_id\":\"ee1e1cdc-f0b6-4353-b761-ada15282a954\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610915,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"ee1e1cdc-f0b6-4353-b761-ada15282a954\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":610915,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"ee1e1cdc-f0b6-4353-b761-ada15282a954\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":610900,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"46958b32-a07f-4aa9-a052-c6a1e3bf70b2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610900} {\"correlation_id\":\"46958b32-a07f-4aa9-a052-c6a1e3bf70b2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610900,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"46958b32-a07f-4aa9-a052-c6a1e3bf70b2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":610900,\"participants\":[{\"id\":997081,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":997082,\"user_id\":18,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"46958b32-a07f-4aa9-a052-c6a1e3bf70b2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"46958b32-a07f-4aa9-a052-c6a1e3bf70b2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"46958b32-a07f-4aa9-a052-c6a1e3bf70b2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"46958b32-a07f-4aa9-a052-c6a1e3bf70b2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"46958b32-a07f-4aa9-a052-c6a1e3bf70b2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"46958b32-a07f-4aa9-a052-c6a1e3bf70b2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"46958b32-a07f-4aa9-a052-c6a1e3bf70b2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"46958b32-a07f-4aa9-a052-c6a1e3bf70b2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":610900,\"team_id\":2,\"email\":\"veselin.kulov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"46958b32-a07f-4aa9-a052-c6a1e3bf70b2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":610900,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"46958b32-a07f-4aa9-a052-c6a1e3bf70b2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610900} {\"correlation_id\":\"46958b32-a07f-4aa9-a052-c6a1e3bf70b2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610900,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"46958b32-a07f-4aa9-a052-c6a1e3bf70b2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":610900,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"46958b32-a07f-4aa9-a052-c6a1e3bf70b2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":610885,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"b9dc6bd0-a276-4d4c-ad70-8cd444119160\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610885} {\"correlation_id\":\"b9dc6bd0-a276-4d4c-ad70-8cd444119160\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610885,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"b9dc6bd0-a276-4d4c-ad70-8cd444119160\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":610885,\"participants\":[{\"id\":997051,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":997052,\"user_id\":18,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"b9dc6bd0-a276-4d4c-ad70-8cd444119160\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"b9dc6bd0-a276-4d4c-ad70-8cd444119160\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"b9dc6bd0-a276-4d4c-ad70-8cd444119160\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"b9dc6bd0-a276-4d4c-ad70-8cd444119160\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"b9dc6bd0-a276-4d4c-ad70-8cd444119160\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"b9dc6bd0-a276-4d4c-ad70-8cd444119160\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"b9dc6bd0-a276-4d4c-ad70-8cd444119160\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"b9dc6bd0-a276-4d4c-ad70-8cd444119160\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":610885,\"team_id\":2,\"email\":\"veselin.kulov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"b9dc6bd0-a276-4d4c-ad70-8cd444119160\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":610885,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"b9dc6bd0-a276-4d4c-ad70-8cd444119160\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610885} {\"correlation_id\":\"b9dc6bd0-a276-4d4c-ad70-8cd444119160\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610885,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"b9dc6bd0-a276-4d4c-ad70-8cd444119160\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":610885,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"b9dc6bd0-a276-4d4c-ad70-8cd444119160\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":610878,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"117202f1-4432-416d-922c-d7cd0f2ae201\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610878} {\"correlation_id\":\"117202f1-4432-416d-922c-d7cd0f2ae201\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610878,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"117202f1-4432-416d-922c-d7cd0f2ae201\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":610878,\"participants\":[{\"id\":997035,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":997036,\"user_id\":18,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"117202f1-4432-416d-922c-d7cd0f2ae201\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":391,\"provider\":\"office\",\"responseBody\":\"{\\\"error\\\":\\\"invalid_client\\\",\\\"error_description\\\":\\\"AADSTS7000215: Invalid client secret provided. Ensure the secret being sent in the request is the client secret value, not the client secret ID, for a secret added to app 'bbcbb2ef-6200-4fae-82bd-d81f5dd738da'. Trace ID: 72b211a8-6a7c-4954-b720-f1a47e981b00 Correlation ID: 5f9781fe-b28a-44ce-94e3-42c683cdfd60 Timestamp: 2026-05-11 10:43:40Z\\\",\\\"error_codes\\\":[7000215],\\\"timestamp\\\":\\\"2026-05-11 10:43:40Z\\\",\\\"trace_id\\\":\\\"72b211a8-6a7c-4954-b720-f1a47e981b00\\\",\\\"correlation_id\\\":\\\"5f9781fe-b28a-44ce-94e3-42c683cdfd60\\\",\\\"error_uri\\\":\\\"https://login.microsoftonline.com/error?code=7000215\\\"}\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:40] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":391,\"provider\":\"office\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"117202f1-4432-416d-922c-d7cd0f2ae201\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"117202f1-4432-416d-922c-d7cd0f2ae201\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"117202f1-4432-416d-922c-d7cd0f2ae201\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"117202f1-4432-416d-922c-d7cd0f2ae201\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1271,\"provider\":\"office\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1271,\"provider\":\"office\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:40] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"117202f1-4432-416d-922c-d7cd0f2ae201\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1271,\"provider\":\"office\",\"refreshToken\":\"118cde2c06993147b07ccaec4cbcd5026a819dea6c71081166a492933e392afb\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"117202f1-4432-416d-922c-d7cd0f2ae201\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"117202f1-4432-416d-922c-d7cd0f2ae201\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":610878,\"team_id\":2,\"email\":\"veselin.kulov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"117202f1-4432-416d-922c-d7cd0f2ae201\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":610878,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"117202f1-4432-416d-922c-d7cd0f2ae201\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610878} {\"correlation_id\":\"117202f1-4432-416d-922c-d7cd0f2ae201\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610878,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"117202f1-4432-416d-922c-d7cd0f2ae201\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":610878,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"117202f1-4432-416d-922c-d7cd0f2ae201\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Google Calendar] Failed to watch channel for calendar {\"calendarId\":\"a33076c1-8d97-431a-99f0-85c9524e118b\",\"code\":400,\"reason\":\"{\n \\\"error\\\": {\n \\\"errors\\\": [\n {\n \\\"domain\\\": \\\"global\\\",\n \\\"reason\\\": \\\"push.webhookUrlNotHttps\\\",\n \\\"message\\\": \\\"WebHook callback must be HTTPS: /webhook/calendar/google?resourceType=event\\\"\n }\n ],\n \\\"code\\\": 400,\n \\\"message\\\": \\\"WebHook callback must be HTTPS: /webhook/calendar/google?resourceType=event\\\"\n }\n}\"} {\"correlation_id\":\"0fdd7dce-04e2-4696-ba87-8089a739c81c\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:40] local.WARNING: [Calendar] Sync failed {\"calendarId\":\"a33076c1-8d97-431a-99f0-85c9524e118b\",\"code\":400,\"reason\":\"{\n \\\"error\\\": {\n \\\"errors\\\": [\n {\n \\\"domain\\\": \\\"global\\\",\n \\\"reason\\\": \\\"push.webhookUrlNotHttps\\\",\n \\\"message\\\": \\\"WebHook callback must be HTTPS: /webhook/calendar/google?resourceType=event\\\"\n }\n ],\n \\\"code\\\": 400,\n \\\"message\\\": \\\"WebHook callback must be HTTPS: /webhook/calendar/google?resourceType=event\\\"\n }\n}\"} {\"correlation_id\":\"0fdd7dce-04e2-4696-ba87-8089a739c81c\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":610874,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"6264f69a-8461-4d85-9725-ef4c30fb5d35\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610874} {\"correlation_id\":\"6264f69a-8461-4d85-9725-ef4c30fb5d35\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610874,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"6264f69a-8461-4d85-9725-ef4c30fb5d35\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":610874,\"participants\":[{\"id\":997025,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":997026,\"user_id\":18,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"6264f69a-8461-4d85-9725-ef4c30fb5d35\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"6264f69a-8461-4d85-9725-ef4c30fb5d35\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"6264f69a-8461-4d85-9725-ef4c30fb5d35\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"6264f69a-8461-4d85-9725-ef4c30fb5d35\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"6264f69a-8461-4d85-9725-ef4c30fb5d35\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"6264f69a-8461-4d85-9725-ef4c30fb5d35\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"6264f69a-8461-4d85-9725-ef4c30fb5d35\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"6264f69a-8461-4d85-9725-ef4c30fb5d35\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":610874,\"team_id\":2,\"email\":\"veselin.kulov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"6264f69a-8461-4d85-9725-ef4c30fb5d35\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":610874,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"6264f69a-8461-4d85-9725-ef4c30fb5d35\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610874} {\"correlation_id\":\"6264f69a-8461-4d85-9725-ef4c30fb5d35\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610874,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"6264f69a-8461-4d85-9725-ef4c30fb5d35\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":610874,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"6264f69a-8461-4d85-9725-ef4c30fb5d35\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":610867,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"978698d8-e33a-4d4a-b374-04919cae6ac2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610867} {\"correlation_id\":\"978698d8-e33a-4d4a-b374-04919cae6ac2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610867,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"978698d8-e33a-4d4a-b374-04919cae6ac2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":610867,\"participants\":[{\"id\":997011,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":997012,\"user_id\":18,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"978698d8-e33a-4d4a-b374-04919cae6ac2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"978698d8-e33a-4d4a-b374-04919cae6ac2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"978698d8-e33a-4d4a-b374-04919cae6ac2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"978698d8-e33a-4d4a-b374-04919cae6ac2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"978698d8-e33a-4d4a-b374-04919cae6ac2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"978698d8-e33a-4d4a-b374-04919cae6ac2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"978698d8-e33a-4d4a-b374-04919cae6ac2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"978698d8-e33a-4d4a-b374-04919cae6ac2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":610867,\"team_id\":2,\"email\":\"veselin.kulov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"978698d8-e33a-4d4a-b374-04919cae6ac2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":610867,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"978698d8-e33a-4d4a-b374-04919cae6ac2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610867} {\"correlation_id\":\"978698d8-e33a-4d4a-b374-04919cae6ac2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610867,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"978698d8-e33a-4d4a-b374-04919cae6ac2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":610867,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"978698d8-e33a-4d4a-b374-04919cae6ac2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":610764,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"3e294e41-3a19-4afe-87b3-4f7f0336107c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610764} {\"correlation_id\":\"3e294e41-3a19-4afe-87b3-4f7f0336107c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610764,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"3e294e41-3a19-4afe-87b3-4f7f0336107c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":610764,\"participants\":[{\"id\":996951,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":996952,\"user_id\":18,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"3e294e41-3a19-4afe-87b3-4f7f0336107c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"3e294e41-3a19-4afe-87b3-4f7f0336107c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"3e294e41-3a19-4afe-87b3-4f7f0336107c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"3e294e41-3a19-4afe-87b3-4f7f0336107c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"3e294e41-3a19-4afe-87b3-4f7f0336107c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"3e294e41-3a19-4afe-87b3-4f7f0336107c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"3e294e41-3a19-4afe-87b3-4f7f0336107c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"3e294e41-3a19-4afe-87b3-4f7f0336107c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":610764,\"team_id\":2,\"email\":\"veselin.kulov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"3e294e41-3a19-4afe-87b3-4f7f0336107c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":610764,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"3e294e41-3a19-4afe-87b3-4f7f0336107c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610764} {\"correlation_id\":\"3e294e41-3a19-4afe-87b3-4f7f0336107c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610764,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"3e294e41-3a19-4afe-87b3-4f7f0336107c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":610764,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"3e294e41-3a19-4afe-87b3-4f7f0336107c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":610617,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"732b2bbb-ee7f-4bb2-85b1-e6478955544b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610617} {\"correlation_id\":\"732b2bbb-ee7f-4bb2-85b1-e6478955544b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610617,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"732b2bbb-ee7f-4bb2-85b1-e6478955544b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":610617,\"participants\":[{\"id\":996641,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":996642,\"user_id\":18,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"732b2bbb-ee7f-4bb2-85b1-e6478955544b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"732b2bbb-ee7f-4bb2-85b1-e6478955544b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"732b2bbb-ee7f-4bb2-85b1-e6478955544b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"732b2bbb-ee7f-4bb2-85b1-e6478955544b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"732b2bbb-ee7f-4bb2-85b1-e6478955544b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"732b2bbb-ee7f-4bb2-85b1-e6478955544b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"732b2bbb-ee7f-4bb2-85b1-e6478955544b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"732b2bbb-ee7f-4bb2-85b1-e6478955544b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":610617,\"team_id\":2,\"email\":\"veselin.kulov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"732b2bbb-ee7f-4bb2-85b1-e6478955544b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":610617,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"732b2bbb-ee7f-4bb2-85b1-e6478955544b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610617} {\"correlation_id\":\"732b2bbb-ee7f-4bb2-85b1-e6478955544b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610617,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"732b2bbb-ee7f-4bb2-85b1-e6478955544b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":610617,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"732b2bbb-ee7f-4bb2-85b1-e6478955544b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":610539,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"d94f0ac2-c007-4b7d-9fd3-27fc32f9b9a2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610539} {\"correlation_id\":\"d94f0ac2-c007-4b7d-9fd3-27fc32f9b9a2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610539,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"d94f0ac2-c007-4b7d-9fd3-27fc32f9b9a2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":610539,\"participants\":[{\"id\":996485,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":996486,\"user_id\":18,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"d94f0ac2-c007-4b7d-9fd3-27fc32f9b9a2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"d94f0ac2-c007-4b7d-9fd3-27fc32f9b9a2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"d94f0ac2-c007-4b7d-9fd3-27fc32f9b9a2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d94f0ac2-c007-4b7d-9fd3-27fc32f9b9a2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"d94f0ac2-c007-4b7d-9fd3-27fc32f9b9a2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"d94f0ac2-c007-4b7d-9fd3-27fc32f9b9a2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"d94f0ac2-c007-4b7d-9fd3-27fc32f9b9a2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"d94f0ac2-c007-4b7d-9fd3-27fc32f9b9a2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":610539,\"team_id\":2,\"email\":\"veselin.kulov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"d94f0ac2-c007-4b7d-9fd3-27fc32f9b9a2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":610539,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"d94f0ac2-c007-4b7d-9fd3-27fc32f9b9a2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610539} {\"correlation_id\":\"d94f0ac2-c007-4b7d-9fd3-27fc32f9b9a2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610539,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"d94f0ac2-c007-4b7d-9fd3-27fc32f9b9a2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":610539,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"d94f0ac2-c007-4b7d-9fd3-27fc32f9b9a2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":610528,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"c316e103-4e48-4f28-8bc6-206714fc00ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610528} {\"correlation_id\":\"c316e103-4e48-4f28-8bc6-206714fc00ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610528,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"c316e103-4e48-4f28-8bc6-206714fc00ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":610528,\"participants\":[{\"id\":996463,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":996464,\"user_id\":18,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"c316e103-4e48-4f28-8bc6-206714fc00ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"c316e103-4e48-4f28-8bc6-206714fc00ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"c316e103-4e48-4f28-8bc6-206714fc00ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"c316e103-4e48-4f28-8bc6-206714fc00ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"c316e103-4e48-4f28-8bc6-206714fc00ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"c316e103-4e48-4f28-8bc6-206714fc00ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"c316e103-4e48-4f28-8bc6-206714fc00ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"c316e103-4e48-4f28-8bc6-206714fc00ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":610528,\"team_id\":2,\"email\":\"veselin.kulov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"c316e103-4e48-4f28-8bc6-206714fc00ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":610528,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"c316e103-4e48-4f28-8bc6-206714fc00ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610528} {\"correlation_id\":\"c316e103-4e48-4f28-8bc6-206714fc00ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610528,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"c316e103-4e48-4f28-8bc6-206714fc00ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":610528,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"c316e103-4e48-4f28-8bc6-206714fc00ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":610506,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"f83f62c6-544e-4551-8bc2-99f405d022e6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610506} {\"correlation_id\":\"f83f62c6-544e-4551-8bc2-99f405d022e6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610506,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"f83f62c6-544e-4551-8bc2-99f405d022e6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":610506,\"participants\":[{\"id\":996419,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":996420,\"user_id\":18,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"f83f62c6-544e-4551-8bc2-99f405d022e6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"f83f62c6-544e-4551-8bc2-99f405d022e6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"f83f62c6-544e-4551-8bc2-99f405d022e6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"f83f62c6-544e-4551-8bc2-99f405d022e6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"f83f62c6-544e-4551-8bc2-99f405d022e6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"f83f62c6-544e-4551-8bc2-99f405d022e6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"f83f62c6-544e-4551-8bc2-99f405d022e6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"f83f62c6-544e-4551-8bc2-99f405d022e6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1271,\"provider\":\"office\",\"responseBody\":\"{\\\"error\\\":\\\"invalid_client\\\",\\\"error_description\\\":\\\"AADSTS7000215: Invalid client secret provided. Ensure the secret being sent in the request is the client secret value, not the client secret ID, for a secret added to app 'bbcbb2ef-6200-4fae-82bd-d81f5dd738da'. Trace ID: 8addf5ba-9879-4822-b2bb-e5002b0d2200 Correlation ID: feece826-abbe-4c27-85d0-0cddd6b33c3b Timestamp: 2026-05-11 10:43:41Z\\\",\\\"error_codes\\\":[7000215],\\\"timestamp\\\":\\\"2026-05-11 10:43:41Z\\\",\\\"trace_id\\\":\\\"8addf5ba-9879-4822-b2bb-e5002b0d2200\\\",\\\"correlation_id\\\":\\\"feece826-abbe-4c27-85d0-0cddd6b33c3b\\\",\\\"error_uri\\\":\\\"https://login.microsoftonline.com/error?code=7000215\\\"}\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1271,\"provider\":\"office\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":610506,\"team_id\":2,\"email\":\"veselin.kulov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"f83f62c6-544e-4551-8bc2-99f405d022e6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":610506,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"f83f62c6-544e-4551-8bc2-99f405d022e6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610506} {\"correlation_id\":\"f83f62c6-544e-4551-8bc2-99f405d022e6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610506,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"f83f62c6-544e-4551-8bc2-99f405d022e6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1351,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":610506,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"f83f62c6-544e-4551-8bc2-99f405d022e6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1351,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1351,\"provider\":\"google\",\"refreshToken\":\"4271d15b9e60a606439caddc68337f783e472c85b03dacff14d1b6dfded9051c\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":610497,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"854a8b20-3fc0-4ea6-a7c0-e0ddd599dd3b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610497} {\"correlation_id\":\"854a8b20-3fc0-4ea6-a7c0-e0ddd599dd3b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610497,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"854a8b20-3fc0-4ea6-a7c0-e0ddd599dd3b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":610497,\"participants\":[{\"id\":996401,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":996402,\"user_id\":18,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"854a8b20-3fc0-4ea6-a7c0-e0ddd599dd3b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1351,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Bad Request\"}} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1351,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"854a8b20-3fc0-4ea6-a7c0-e0ddd599dd3b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"854a8b20-3fc0-4ea6-a7c0-e0ddd599dd3b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"854a8b20-3fc0-4ea6-a7c0-e0ddd599dd3b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"854a8b20-3fc0-4ea6-a7c0-e0ddd599dd3b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1366,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1366,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"854a8b20-3fc0-4ea6-a7c0-e0ddd599dd3b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"854a8b20-3fc0-4ea6-a7c0-e0ddd599dd3b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1366,\"provider\":\"google\",\"refreshToken\":\"ae21385059b2eebfd43f68aecd56eccd702a1aabb6598f1f7ab594ed8af491b4\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"854a8b20-3fc0-4ea6-a7c0-e0ddd599dd3b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":610497,\"team_id\":2,\"email\":\"veselin.kulov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"854a8b20-3fc0-4ea6-a7c0-e0ddd599dd3b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":610497,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"854a8b20-3fc0-4ea6-a7c0-e0ddd599dd3b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610497} {\"correlation_id\":\"854a8b20-3fc0-4ea6-a7c0-e0ddd599dd3b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610497,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"854a8b20-3fc0-4ea6-a7c0-e0ddd599dd3b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":610497,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"854a8b20-3fc0-4ea6-a7c0-e0ddd599dd3b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":610490,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"bab4020c-8e0d-42eb-afdb-e6bab67d42c7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610490} {\"correlation_id\":\"bab4020c-8e0d-42eb-afdb-e6bab67d42c7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610490,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"bab4020c-8e0d-42eb-afdb-e6bab67d42c7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":610490,\"participants\":[{\"id\":996385,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":996386,\"user_id\":18,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"bab4020c-8e0d-42eb-afdb-e6bab67d42c7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"bab4020c-8e0d-42eb-afdb-e6bab67d42c7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"bab4020c-8e0d-42eb-afdb-e6bab67d42c7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"bab4020c-8e0d-42eb-afdb-e6bab67d42c7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"bab4020c-8e0d-42eb-afdb-e6bab67d42c7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"bab4020c-8e0d-42eb-afdb-e6bab67d42c7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"bab4020c-8e0d-42eb-afdb-e6bab67d42c7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"bab4020c-8e0d-42eb-afdb-e6bab67d42c7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":610490,\"team_id\":2,\"email\":\"veselin.kulov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"bab4020c-8e0d-42eb-afdb-e6bab67d42c7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":610490,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"bab4020c-8e0d-42eb-afdb-e6bab67d42c7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610490} {\"correlation_id\":\"bab4020c-8e0d-42eb-afdb-e6bab67d42c7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610490,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"bab4020c-8e0d-42eb-afdb-e6bab67d42c7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":610490,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"bab4020c-8e0d-42eb-afdb-e6bab67d42c7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1366,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Bad Request\"}} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1366,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":610470,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"55223d65-4d62-42ac-994b-ae4c8f27cbef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1115,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610470} {\"correlation_id\":\"55223d65-4d62-42ac-994b-ae4c8f27cbef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1115,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610470,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"55223d65-4d62-42ac-994b-ae4c8f27cbef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":610470,\"participants\":[{\"id\":996369,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":996370,\"user_id\":18,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"55223d65-4d62-42ac-994b-ae4c8f27cbef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: Calendar sync job dispatched {\"calendar_id\":378} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1421,\"provider\":\"office\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1421,\"provider\":\"office\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"55223d65-4d62-42ac-994b-ae4c8f27cbef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"55223d65-4d62-42ac-994b-ae4c8f27cbef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"55223d65-4d62-42ac-994b-ae4c8f27cbef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: Calendar sync job dispatched {\"calendar_id\":504} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"55223d65-4d62-42ac-994b-ae4c8f27cbef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"55223d65-4d62-42ac-994b-ae4c8f27cbef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"55223d65-4d62-42ac-994b-ae4c8f27cbef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"55223d65-4d62-42ac-994b-ae4c8f27cbef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.NOTICE: Calendar sync end {\"retrieved_calendars\":31,\"processed_calendars\":3} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"calendar:sync\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":610470,\"team_id\":2,\"email\":\"veselin.kulov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"55223d65-4d62-42ac-994b-ae4c8f27cbef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":610470,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"55223d65-4d62-42ac-994b-ae4c8f27cbef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610470} {\"correlation_id\":\"55223d65-4d62-42ac-994b-ae4c8f27cbef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610470,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"55223d65-4d62-42ac-994b-ae4c8f27cbef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":610470,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"55223d65-4d62-42ac-994b-ae4c8f27cbef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":610462,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"0108a9c2-fb98-43fc-ad71-084c53b40936\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610462} {\"correlation_id\":\"0108a9c2-fb98-43fc-ad71-084c53b40936\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610462,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"0108a9c2-fb98-43fc-ad71-084c53b40936\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":610462,\"participants\":[{\"id\":996353,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":996354,\"user_id\":18,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"0108a9c2-fb98-43fc-ad71-084c53b40936\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"0108a9c2-fb98-43fc-ad71-084c53b40936\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"0108a9c2-fb98-43fc-ad71-084c53b40936\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"0108a9c2-fb98-43fc-ad71-084c53b40936\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"0108a9c2-fb98-43fc-ad71-084c53b40936\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"0108a9c2-fb98-43fc-ad71-084c53b40936\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"0108a9c2-fb98-43fc-ad71-084c53b40936\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"0108a9c2-fb98-43fc-ad71-084c53b40936\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":610462,\"team_id\":2,\"email\":\"veselin.kulov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"0108a9c2-fb98-43fc-ad71-084c53b40936\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":610462,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"0108a9c2-fb98-43fc-ad71-084c53b40936\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610462} {\"correlation_id\":\"0108a9c2-fb98-43fc-ad71-084c53b40936\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610462,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"0108a9c2-fb98-43fc-ad71-084c53b40936\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":610462,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"0108a9c2-fb98-43fc-ad71-084c53b40936\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":610451,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"d1fa1d3a-52aa-496e-8905-56f7157299b1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610451} {\"correlation_id\":\"d1fa1d3a-52aa-496e-8905-56f7157299b1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610451,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"d1fa1d3a-52aa-496e-8905-56f7157299b1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":610451,\"participants\":[{\"id\":996340,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":996341,\"user_id\":18,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"d1fa1d3a-52aa-496e-8905-56f7157299b1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"d1fa1d3a-52aa-496e-8905-56f7157299b1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"d1fa1d3a-52aa-496e-8905-56f7157299b1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d1fa1d3a-52aa-496e-8905-56f7157299b1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"d1fa1d3a-52aa-496e-8905-56f7157299b1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"d1fa1d3a-52aa-496e-8905-56f7157299b1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"d1fa1d3a-52aa-496e-8905-56f7157299b1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"d1fa1d3a-52aa-496e-8905-56f7157299b1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":610451,\"team_id\":2,\"email\":\"veselin.kulov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"d1fa1d3a-52aa-496e-8905-56f7157299b1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":610451,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"d1fa1d3a-52aa-496e-8905-56f7157299b1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610451} {\"correlation_id\":\"d1fa1d3a-52aa-496e-8905-56f7157299b1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610451,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"d1fa1d3a-52aa-496e-8905-56f7157299b1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":610451,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"d1fa1d3a-52aa-496e-8905-56f7157299b1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":610438,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"da3f7962-a65d-4bfe-80bf-c33b4ccd36f5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610438} {\"correlation_id\":\"da3f7962-a65d-4bfe-80bf-c33b4ccd36f5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610438,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"da3f7962-a65d-4bfe-80bf-c33b4ccd36f5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":610438,\"participants\":[{\"id\":996320,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":996321,\"user_id\":18,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"da3f7962-a65d-4bfe-80bf-c33b4ccd36f5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"da3f7962-a65d-4bfe-80bf-c33b4ccd36f5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"da3f7962-a65d-4bfe-80bf-c33b4ccd36f5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"da3f7962-a65d-4bfe-80bf-c33b4ccd36f5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"da3f7962-a65d-4bfe-80bf-c33b4ccd36f5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"da3f7962-a65d-4bfe-80bf-c33b4ccd36f5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"da3f7962-a65d-4bfe-80bf-c33b4ccd36f5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"da3f7962-a65d-4bfe-80bf-c33b4ccd36f5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":610438,\"team_id\":2,\"email\":\"veselin.kulov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"da3f7962-a65d-4bfe-80bf-c33b4ccd36f5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":610438,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"da3f7962-a65d-4bfe-80bf-c33b4ccd36f5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610438} {\"correlation_id\":\"da3f7962-a65d-4bfe-80bf-c33b4ccd36f5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610438,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"da3f7962-a65d-4bfe-80bf-c33b4ccd36f5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":610438,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"da3f7962-a65d-4bfe-80bf-c33b4ccd36f5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":610426,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"da7b40c8-e233-4cf9-a133-31f12397ad10\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610426} {\"correlation_id\":\"da7b40c8-e233-4cf9-a133-31f12397ad10\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610426,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"da7b40c8-e233-4cf9-a133-31f12397ad10\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":610426,\"participants\":[{\"id\":996306,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":996307,\"user_id\":18,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"da7b40c8-e233-4cf9-a133-31f12397ad10\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"da7b40c8-e233-4cf9-a133-31f12397ad10\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"da7b40c8-e233-4cf9-a133-31f12397ad10\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"da7b40c8-e233-4cf9-a133-31f12397ad10\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"da7b40c8-e233-4cf9-a133-31f12397ad10\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"da7b40c8-e233-4cf9-a133-31f12397ad10\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"da7b40c8-e233-4cf9-a133-31f12397ad10\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"da7b40c8-e233-4cf9-a133-31f12397ad10\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":610426,\"team_id\":2,\"email\":\"veselin.kulov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"da7b40c8-e233-4cf9-a133-31f12397ad10\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":610426,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"da7b40c8-e233-4cf9-a133-31f12397ad10\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610426} {\"correlation_id\":\"da7b40c8-e233-4cf9-a133-31f12397ad10\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610426,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"da7b40c8-e233-4cf9-a133-31f12397ad10\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":610426,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"da7b40c8-e233-4cf9-a133-31f12397ad10\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":610403,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"4e1aa6ad-3e68-46fe-8129-19feb67cee5f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610403} {\"correlation_id\":\"4e1aa6ad-3e68-46fe-8129-19feb67cee5f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610403,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"4e1aa6ad-3e68-46fe-8129-19feb67cee5f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":610403,\"participants\":[{\"id\":996282,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":996283,\"user_id\":18,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"4e1aa6ad-3e68-46fe-8129-19feb67cee5f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"4e1aa6ad-3e68-46fe-8129-19feb67cee5f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"4e1aa6ad-3e68-46fe-8129-19feb67cee5f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4e1aa6ad-3e68-46fe-8129-19feb67cee5f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"4e1aa6ad-3e68-46fe-8129-19feb67cee5f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"4e1aa6ad-3e68-46fe-8129-19feb67cee5f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"4e1aa6ad-3e68-46fe-8129-19feb67cee5f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"4e1aa6ad-3e68-46fe-8129-19feb67cee5f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":610403,\"team_id\":2,\"email\":\"veselin.kulov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"4e1aa6ad-3e68-46fe-8129-19feb67cee5f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":610403,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"4e1aa6ad-3e68-46fe-8129-19feb67cee5f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610403} {\"correlation_id\":\"4e1aa6ad-3e68-46fe-8129-19feb67cee5f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610403,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"4e1aa6ad-3e68-46fe-8129-19feb67cee5f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":610403,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"4e1aa6ad-3e68-46fe-8129-19feb67cee5f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":610400,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":350,\"stage_id\":34}} {\"correlation_id\":\"c137d466-9e9c-4af1-82c0-9f8639805989\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610400} {\"correlation_id\":\"c137d466-9e9c-4af1-82c0-9f8639805989\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610400,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"c137d466-9e9c-4af1-82c0-9f8639805989\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":610400,\"participants\":[{\"id\":996275,\"user_id\":1460,\"contact_id\":null,\"lead_id\":null},{\"id\":996276,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":996277,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null}]} {\"correlation_id\":\"c137d466-9e9c-4af1-82c0-9f8639805989\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"c137d466-9e9c-4af1-82c0-9f8639805989\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"c137d466-9e9c-4af1-82c0-9f8639805989\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"c137d466-9e9c-4af1-82c0-9f8639805989\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"c137d466-9e9c-4af1-82c0-9f8639805989\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":610400,\"team_id\":2,\"email\":\"aneliya.angelova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"c137d466-9e9c-4af1-82c0-9f8639805989\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":610400,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"c137d466-9e9c-4af1-82c0-9f8639805989\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: ProspectCache - Searching DB for opportunity by owner {\"account_id\":244,\"contact_id\":4487,\"owner_id\":1460} {\"correlation_id\":\"c137d466-9e9c-4af1-82c0-9f8639805989\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: ProspectCache - Fallback DB opportunity search {\"account_id\":244,\"contact_id\":4487} {\"correlation_id\":\"c137d466-9e9c-4af1-82c0-9f8639805989\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: ProspectCache - Opportunity DB search results {\"account_id\":244,\"contact_id\":4487,\"opportunity_id\":350} {\"correlation_id\":\"c137d466-9e9c-4af1-82c0-9f8639805989\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"c137d466-9e9c-4af1-82c0-9f8639805989\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":610400,\"participants_processed\":3,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"c137d466-9e9c-4af1-82c0-9f8639805989\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610400} {\"correlation_id\":\"c137d466-9e9c-4af1-82c0-9f8639805989\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610400,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"c137d466-9e9c-4af1-82c0-9f8639805989\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":610400,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":350,\"stage_id\":34} {\"correlation_id\":\"c137d466-9e9c-4af1-82c0-9f8639805989\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1115,\"provider\":\"google\"} {\"correlation_id\":\"003a1645-a7b4-447f-8217-b1f109d902a1\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:42] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1115,\"provider\":\"google\"} {\"correlation_id\":\"003a1645-a7b4-447f-8217-b1f109d902a1\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:42] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"003a1645-a7b4-447f-8217-b1f109d902a1\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:42] local.INFO: [Calendar] Processing sync {\"calendarId\":\"2676cb6d-f86c-427e-bf78-591e388e3c1e\",\"from\":null,\"to\":null,\"delta\":\"CJ_x49O3jpIDEJ_x49O3jpIDGAUgw67KlwMow67KlwM=\",\"last_sync\":\"2026-01-19 07:48:40\",\"dateMode\":\"daily\"} {\"correlation_id\":\"003a1645-a7b4-447f-8217-b1f109d902a1\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:42] local.WARNING: [Pipedrive] Account not connected for user {\"userId\":\"e6538737-e7b4-455f-a37a-3e79b665a220\",\"account\":{\"Jiminny\\\\Models\\\\SocialAccount\":{\"id\":1116,\"sociable_id\":241,\"provider_user_id\":\"19555731\",\"expires\":1775683749,\"refresh_token_expires\":null,\"provider\":\"pipedrive\",\"state\":\"full-refresh\",\"auth_scope\":\"base,deals:full,activities:full,contacts:full,search:read\",\"retry_after\":null,\"created_at\":\"2023-09-08 09:44:29\",\"updated_at\":\"2026-04-08 22:58:34\"}}} {\"correlation_id\":\"003a1645-a7b4-447f-8217-b1f109d902a1\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:42] local.INFO: [CrmOwnerResolver] Integration owner is not connected, attempting team members {\"crm_provider\":\"pipedrive\",\"crm_owner\":241,\"team_id\":19} {\"correlation_id\":\"003a1645-a7b4-447f-8217-b1f109d902a1\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:42] local.INFO: [CrmOwnerResolver] No team members found with active crm connection {\"crm_provider\":\"pipedrive\",\"team_id\":19} {\"correlation_id\":\"003a1645-a7b4-447f-8217-b1f109d902a1\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:42] local.INFO: [CrmOwnerResolver] No team member found with active crm connection {\"crm_provider\":\"pipedrive\",\"team_id\":19} {\"correlation_id\":\"003a1645-a7b4-447f-8217-b1f109d902a1\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:42] local.WARNING: [Calendar] CRM disconnected for user so events will not be matched {\"provider\":\"pipedrive\",\"user_id\":241,\"message\":\"Your Pipedrive account has become disconnected. Please login to Jiminny to reconnect.\"} {\"correlation_id\":\"003a1645-a7b4-447f-8217-b1f109d902a1\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:42] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1115,\"provider\":\"google\"} {\"correlation_id\":\"003a1645-a7b4-447f-8217-b1f109d902a1\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:42] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1115,\"provider\":\"google\"} {\"correlation_id\":\"003a1645-a7b4-447f-8217-b1f109d902a1\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:42] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"003a1645-a7b4-447f-8217-b1f109d902a1\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:42] local.INFO: [Google Calendar] Failed to watch channel for calendar {\"calendarId\":\"2676cb6d-f86c-427e-bf78-591e388e3c1e\",\"code\":400,\"reason\":\"{\n \\\"error\\\": {\n \\\"errors\\\": [\n {\n \\\"domain\\\": \\\"global\\\",\n \\\"reason\\\": \\\"push.webhookUrlNotHttps\\\",\n \\\"message\\\": \\\"WebHook callback must be HTTPS: /webhook/calendar/google?resourceType=event\\\"\n }\n ],\n \\\"code\\\": 400,\n \\\"message\\\": \\\"WebHook callback must be HTTPS: /webhook/calendar/google?resourceType=event\\\"\n }\n}\"} {\"correlation_id\":\"003a1645-a7b4-447f-8217-b1f109d902a1\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:42] local.WARNING: [Calendar] Sync failed {\"calendarId\":\"2676cb6d-f86c-427e-bf78-591e388e3c1e\",\"code\":400,\"reason\":\"{\n \\\"error\\\": {\n \\\"errors\\\": [\n {\n \\\"domain\\\": \\\"global\\\",\n \\\"reason\\\": \\\"push.webhookUrlNotHttps\\\",\n \\\"message\\\": \\\"WebHook callback must be HTTPS: /webhook/calendar/google?resourceType=event\\\"\n }\n ],\n \\\"code\\\": 400,\n \\\"message\\\": \\\"WebHook callback must be HTTPS: /webhook/calendar/google?resourceType=event\\\"\n }\n}\"} {\"correlation_id\":\"003a1645-a7b4-447f-8217-b1f109d902a1\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:42] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1421,\"provider\":\"office\"} {\"correlation_id\":\"f66a7935-f1a6-4d93-b53d-1464e6e79019\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:42] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1421,\"provider\":\"office\"} {\"correlation_id\":\"f66a7935-f1a6-4d93-b53d-1464e6e79019\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:42] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"f66a7935-f1a6-4d93-b53d-1464e6e79019\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:42] local.INFO: [Calendar] Processing sync {\"calendarId\":\"9e8b1a2c-1a8f-42bd-b161-810fc0baf540\",\"from\":null,\"to\":null,\"delta\":\"R0usmcdvmMuZCBYV0hguCLlvcOB4kXlhlC7KgH1SnZwTrZ3faZv1fXPQqJhxe_L9AxWWlb-wASsjGiiWlhsBUg9MFb3ZdlAYerVV_ZirRPbsKWCxEXhybD90arJmok_M4ecGFUQ9_BIGu-c6RAnJy2TRKZ7gPTsJi_8TGceGAuqimlhm4G4mjDLvYVVwImjjU7M3xJvUzL47dLOGNTJCww.k1TST0VEYCgbFOkwa3ysYMi100FtVfkzfqlXLnV6gPg\",\"last_sync\":\"2026-05-11 06:13:36\",\"dateMode\":\"daily\"} {\"correlation_id\":\"f66a7935-f1a6-4d93-b53d-1464e6e79019\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:42] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"f66a7935-f1a6-4d93-b53d-1464e6e79019\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:42] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"f66a7935-f1a6-4d93-b53d-1464e6e79019\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:42] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"f66a7935-f1a6-4d93-b53d-1464e6e79019\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:42] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":89,\"team_id\":2} {\"correlation_id\":\"f66a7935-f1a6-4d93-b53d-1464e6e79019\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:42] local.INFO: [MS Office Calendar] Skipping delta sync for daily mode {\"calendarId\":\"9e8b1a2c-1a8f-42bd-b161-810fc0baf540\"} {\"correlation_id\":\"f66a7935-f1a6-4d93-b53d-1464e6e79019\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}","depth":4,"on_screen":true,"value":"[2026-05-11 10:41:16] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"ee9b9096-b1f1-4e2b-ac17-af0003eb00c0\",\"trace_id\":\"8904c61b-d5b4-44cc-91bf-68f0f73222c0\"}\n[2026-05-11 10:41:16] local.INFO: [EmailSchedule] STARTING batch process {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"ee9b9096-b1f1-4e2b-ac17-af0003eb00c0\",\"trace_id\":\"8904c61b-d5b4-44cc-91bf-68f0f73222c0\"}\n[2026-05-11 10:41:17] local.INFO: [EmailSchedule] FINISHED batch process {\"host\":\"docker_lamp_1\",\"processed\":0} {\"correlation_id\":\"ee9b9096-b1f1-4e2b-ac17-af0003eb00c0\",\"trace_id\":\"8904c61b-d5b4-44cc-91bf-68f0f73222c0\"}\n[2026-05-11 10:41:17] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":60.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"ee9b9096-b1f1-4e2b-ac17-af0003eb00c0\",\"trace_id\":\"8904c61b-d5b4-44cc-91bf-68f0f73222c0\"}\n[2026-05-11 10:41:21] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"crm:sync-hubspot-objects\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"13c8d2de-a598-41db-b4ca-9799a4bee946\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:21] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"crm:sync-hubspot-objects\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":60.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"13c8d2de-a598-41db-b4ca-9799a4bee946\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:21] local.INFO: [SyncHubspotObjects] Starting sync {\"team\":\"b2b115eb-93ce-4d1b-929c-173757df8fba\",\"usage\":24894088,\"real_usage\":65011712,\"pid\":62058} {\"correlation_id\":\"81433689-3fcd-4128-8b21-068ce5ee75be\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:21] local.WARNING: [HubSpot] Account not connected for user {\"userId\":\"33e34a7a-1c02-4f04-87ac-22c3a385e6e3\",\"account\":{\"Jiminny\\\\Models\\\\SocialAccount\":{\"id\":306,\"sociable_id\":109,\"provider_user_id\":\"11348452\",\"expires\":1701077403,\"refresh_token_expires\":null,\"provider\":\"hubspot\",\"state\":\"full-refresh\",\"auth_scope\":null,\"retry_after\":null,\"created_at\":\"2020-09-01 16:59:04\",\"updated_at\":\"2023-11-27 09:30:03\"}}} {\"correlation_id\":\"81433689-3fcd-4128-8b21-068ce5ee75be\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:21] local.INFO: [CrmOwnerResolver] Integration owner is not connected, attempting team members {\"crm_provider\":\"hubspot\",\"crm_owner\":109,\"team_id\":29} {\"correlation_id\":\"81433689-3fcd-4128-8b21-068ce5ee75be\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:21] local.INFO: [CrmOwnerResolver] No team members found with active crm connection {\"crm_provider\":\"hubspot\",\"team_id\":29} {\"correlation_id\":\"81433689-3fcd-4128-8b21-068ce5ee75be\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:21] local.INFO: [CrmOwnerResolver] No team member found with active crm connection {\"crm_provider\":\"hubspot\",\"team_id\":29} {\"correlation_id\":\"81433689-3fcd-4128-8b21-068ce5ee75be\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:21] local.INFO: [SyncHubspotObjects] Sync finished {\"team\":\"b2b115eb-93ce-4d1b-929c-173757df8fba\",\"provider\":\"hubspot\",\"status\":\"disconnected\",\"duration_ms\":35.85,\"usage\":24956128,\"real_usage\":65011712,\"pid\":62058,\"reason\":\"Your HubSpot account has become disconnected. Please login to Jiminny to reconnect.\"} {\"correlation_id\":\"81433689-3fcd-4128-8b21-068ce5ee75be\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:21] local.INFO: [SyncHubspotObjects] Starting sync {\"team\":\"abae74b8-bfa8-4383-9a7f-89f4bf2bdbb4\",\"usage\":24914344,\"real_usage\":65011712,\"pid\":62058} {\"correlation_id\":\"d2cb4aff-14c0-474f-89f2-f5c69dafe934\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:21] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"d2cb4aff-14c0-474f-89f2-f5c69dafe934\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:21] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"d2cb4aff-14c0-474f-89f2-f5c69dafe934\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:21] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d2cb4aff-14c0-474f-89f2-f5c69dafe934\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:21] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"d2cb4aff-14c0-474f-89f2-f5c69dafe934\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:21] local.INFO: [HubSpot] Syncing opportunities using strategy: lastModified {\"team\":2} {\"correlation_id\":\"d2cb4aff-14c0-474f-89f2-f5c69dafe934\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:21] local.INFO: [Hubspot] Pagination completed {\"team_id\":2,\"endpoint\":\"https://api.hubapi.com/crm/v3/objects/deals/search\",\"total_requests\":1,\"total_records_fetched\":0,\"total_elapsed_seconds\":0.26,\"average_seconds_per_request\":0.26} {\"correlation_id\":\"d2cb4aff-14c0-474f-89f2-f5c69dafe934\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:21] local.INFO: [HubSpot] Synced opportunities {\"team\":2,\"strategies\":\"lastModified\",\"sync_count\":0,\"total\":0,\"last_synced_id\":null,\"duration_ms\":288.63} {\"correlation_id\":\"d2cb4aff-14c0-474f-89f2-f5c69dafe934\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:21] local.INFO: [SyncHubspotObjects] Sync finished {\"team\":\"abae74b8-bfa8-4383-9a7f-89f4bf2bdbb4\",\"provider\":\"hubspot\",\"status\":\"completed\",\"duration_ms\":324.99,\"usage\":24978696,\"real_usage\":65011712,\"pid\":62058} {\"correlation_id\":\"d2cb4aff-14c0-474f-89f2-f5c69dafe934\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:22] local.INFO: [SyncHubspotObjects] Starting sync {\"team\":\"c6b9d6b0-b48d-4832-a68c-a57d60651888\",\"usage\":24956624,\"real_usage\":65011712,\"pid\":62058} {\"correlation_id\":\"955d9400-2f7f-4170-be31-43bafa1ef3bd\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:22] local.WARNING: [HubSpot] Account not connected for user {\"userId\":\"71e3aac5-fb66-47c5-a236-2d051ae3e319\",\"account\":null} {\"correlation_id\":\"955d9400-2f7f-4170-be31-43bafa1ef3bd\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:22] local.INFO: [CrmOwnerResolver] Integration owner is not connected, attempting team members {\"crm_provider\":\"hubspot\",\"crm_owner\":256,\"team_id\":49} {\"correlation_id\":\"955d9400-2f7f-4170-be31-43bafa1ef3bd\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:22] local.INFO: [CrmOwnerResolver] No team members found with active crm connection {\"crm_provider\":\"hubspot\",\"team_id\":49} {\"correlation_id\":\"955d9400-2f7f-4170-be31-43bafa1ef3bd\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:22] local.INFO: [CrmOwnerResolver] No team member found with active crm connection {\"crm_provider\":\"hubspot\",\"team_id\":49} {\"correlation_id\":\"955d9400-2f7f-4170-be31-43bafa1ef3bd\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:22] local.INFO: [SyncHubspotObjects] Sync finished {\"team\":\"c6b9d6b0-b48d-4832-a68c-a57d60651888\",\"provider\":\"hubspot\",\"status\":\"disconnected\",\"duration_ms\":17.51,\"usage\":24941048,\"real_usage\":65011712,\"pid\":62058,\"reason\":\"Social account for HubSpot cannot be found. Please login to Jiminny to connect.\"} {\"correlation_id\":\"955d9400-2f7f-4170-be31-43bafa1ef3bd\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:22] local.INFO: [SyncHubspotObjects] Starting sync {\"team\":\"b2d49a54-b645-4637-a7ae-a86cfce6e8e4\",\"usage\":24901688,\"real_usage\":65011712,\"pid\":62058} {\"correlation_id\":\"6d0b5717-9160-4c2f-ad12-56e42a2c544a\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:22] local.WARNING: [HubSpot] Account not connected for user {\"userId\":\"2ac0447f-3c8c-4ce0-baeb-b63ddb76fa9b\",\"account\":null} {\"correlation_id\":\"6d0b5717-9160-4c2f-ad12-56e42a2c544a\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:22] local.INFO: [CrmOwnerResolver] Integration owner is not connected, attempting team members {\"crm_provider\":\"hubspot\",\"crm_owner\":130,\"team_id\":42} {\"correlation_id\":\"6d0b5717-9160-4c2f-ad12-56e42a2c544a\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:22] local.INFO: [CrmOwnerResolver] No team members found with active crm connection {\"crm_provider\":\"hubspot\",\"team_id\":42} {\"correlation_id\":\"6d0b5717-9160-4c2f-ad12-56e42a2c544a\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:22] local.INFO: [CrmOwnerResolver] No team member found with active crm connection {\"crm_provider\":\"hubspot\",\"team_id\":42} {\"correlation_id\":\"6d0b5717-9160-4c2f-ad12-56e42a2c544a\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:22] local.INFO: [SyncHubspotObjects] Sync finished {\"team\":\"b2d49a54-b645-4637-a7ae-a86cfce6e8e4\",\"provider\":\"hubspot\",\"status\":\"disconnected\",\"duration_ms\":10.99,\"usage\":24944632,\"real_usage\":65011712,\"pid\":62058,\"reason\":\"Social account for HubSpot cannot be found. Please login to Jiminny to connect.\"} {\"correlation_id\":\"6d0b5717-9160-4c2f-ad12-56e42a2c544a\",\"trace_id\":\"85901fdb-28fb-4611-bb3c-85a8bc158a75\"}\n[2026-05-11 10:41:39] local.INFO: [HubSpot Journal Polling] No data {\"correlation_id\":\"1f8f11e6-0cf5-49dd-b8d7-56f51ec27f8e\",\"trace_id\":\"0f08076e-448d-487e-99a0-e73dae5c0ad4\"}\n[2026-05-11 10:41:39] local.WARNING: [HubSpot Journal Polling] Maximum empty results reached, stopping {\"empty_results\":5,\"max_empty_results\":5} {\"correlation_id\":\"1f8f11e6-0cf5-49dd-b8d7-56f51ec27f8e\",\"trace_id\":\"0f08076e-448d-487e-99a0-e73dae5c0ad4\"}\n[2026-05-11 10:41:39] local.WARNING: [HubSpot Journal Polling] Maximum empty results reached, stopping {\"empty_results\":5,\"max_empty_results\":5} {\"correlation_id\":\"1f8f11e6-0cf5-49dd-b8d7-56f51ec27f8e\",\"trace_id\":\"0f08076e-448d-487e-99a0-e73dae5c0ad4\"}\n[2026-05-11 10:41:39] local.INFO: [HubSpot Journal Polling] Service ending {\"runtime_seconds\":57,\"total_cycles\":5,\"files_downloaded\":0,\"empty_files\":0,\"other_portal_skipped\":0,\"total_events\":0,\"events_per_file\":0,\"avg_api_ms\":296.8,\"avg_download_ms\":0.0,\"avg_transform_ms\":0.0,\"avg_process_ms\":0.0,\"peak_memory_mb\":99.73} {\"correlation_id\":\"1f8f11e6-0cf5-49dd-b8d7-56f51ec27f8e\",\"trace_id\":\"0f08076e-448d-487e-99a0-e73dae5c0ad4\"}\n[2026-05-11 10:41:39] local.INFO: [HubSpot Journal Polling] Saved offset to database on cleanup {\"offset\":\"019e15a9-9ea0-7da7-87bc-82592e3ccf0d\"} {\"correlation_id\":\"1f8f11e6-0cf5-49dd-b8d7-56f51ec27f8e\",\"trace_id\":\"0f08076e-448d-487e-99a0-e73dae5c0ad4\"}\n[2026-05-11 10:41:39] local.INFO: [HubSpot Journal Polling] Released polling lock {\"correlation_id\":\"1f8f11e6-0cf5-49dd-b8d7-56f51ec27f8e\",\"trace_id\":\"0f08076e-448d-487e-99a0-e73dae5c0ad4\"}\n[2026-05-11 10:42:07] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"1aa66921-0a71-4d61-8cd7-0663242dca44\",\"trace_id\":\"f6b1d739-39b6-4043-a81e-a1af2c76d401\"}\n[2026-05-11 10:42:07] local.INFO: [ScheduleBotCommand] Number of activities to be captured: 0 {\"correlation_id\":\"1aa66921-0a71-4d61-8cd7-0663242dca44\",\"trace_id\":\"f6b1d739-39b6-4043-a81e-a1af2c76d401\"}\n[2026-05-11 10:42:07] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":60.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"1aa66921-0a71-4d61-8cd7-0663242dca44\",\"trace_id\":\"f6b1d739-39b6-4043-a81e-a1af2c76d401\"}\n[2026-05-11 10:42:10] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"cc420ba5-39bf-4183-95ae-48962a9c124b\",\"trace_id\":\"8daf6081-5c8a-4fae-98d8-e317d8580bc3\"}\n[2026-05-11 10:42:10] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":60.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"cc420ba5-39bf-4183-95ae-48962a9c124b\",\"trace_id\":\"8daf6081-5c8a-4fae-98d8-e317d8580bc3\"}\n[2026-05-11 10:42:13] local.NOTICE: Monitoring start {\"correlation_id\":\"50696062-587c-4c43-8075-7229916ea46f\",\"trace_id\":\"b58f754c-906f-413b-80e6-f89189dbe078\"}\n[2026-05-11 10:42:13] local.NOTICE: Monitoring end {\"correlation_id\":\"50696062-587c-4c43-8075-7229916ea46f\",\"trace_id\":\"b58f754c-906f-413b-80e6-f89189dbe078\"}\n[2026-05-11 10:42:17] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"f70656cd-6de3-4f8b-bb9d-74a941c552c4\",\"trace_id\":\"c780d8e3-2abd-41a3-a9dc-7c2be6aa22d4\"}\n[2026-05-11 10:42:17] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":60.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"f70656cd-6de3-4f8b-bb9d-74a941c552c4\",\"trace_id\":\"c780d8e3-2abd-41a3-a9dc-7c2be6aa22d4\"}\n[2026-05-11 10:42:20] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"b2e73601-1777-4ddb-a152-fe85f4c89afc\",\"trace_id\":\"8bac5054-216a-4605-b13b-9efab8ab5341\"}\n[2026-05-11 10:42:20] local.INFO: [EmailSchedule] STARTING batch process {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"b2e73601-1777-4ddb-a152-fe85f4c89afc\",\"trace_id\":\"8bac5054-216a-4605-b13b-9efab8ab5341\"}\n[2026-05-11 10:42:20] local.INFO: [EmailSchedule] FINISHED batch process {\"host\":\"docker_lamp_1\",\"processed\":0} {\"correlation_id\":\"b2e73601-1777-4ddb-a152-fe85f4c89afc\",\"trace_id\":\"8bac5054-216a-4605-b13b-9efab8ab5341\"}\n[2026-05-11 10:42:20] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":60.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"b2e73601-1777-4ddb-a152-fe85f4c89afc\",\"trace_id\":\"8bac5054-216a-4605-b13b-9efab8ab5341\"}\n[2026-05-11 10:42:31] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"conference:monitor:count\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"175d1973-e4ab-47f7-bb25-14bc3f0a1cce\",\"trace_id\":\"723f475a-f2fc-4235-8163-c2a8ed5777e6\"}\n[2026-05-11 10:42:31] local.INFO: Running conference:monitor:count command for activities in (2026-05-11 10:40:00, 2026-05-11 10:42:00] {\"correlation_id\":\"175d1973-e4ab-47f7-bb25-14bc3f0a1cce\",\"trace_id\":\"723f475a-f2fc-4235-8163-c2a8ed5777e6\"}\n[2026-05-11 10:42:31] local.INFO: [conference:monitor:count] No activities found in (2026-05-11 10:40:00, 2026-05-11 10:42:00] {\"correlation_id\":\"175d1973-e4ab-47f7-bb25-14bc3f0a1cce\",\"trace_id\":\"723f475a-f2fc-4235-8163-c2a8ed5777e6\"}\n[2026-05-11 10:42:31] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"conference:monitor:count\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":60.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"175d1973-e4ab-47f7-bb25-14bc3f0a1cce\",\"trace_id\":\"723f475a-f2fc-4235-8163-c2a8ed5777e6\"}\n[2026-05-11 10:42:36] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:create\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"4243f43b-9d70-4dd0-9c98-3122eb9445ed\",\"trace_id\":\"0c811e1d-ed7e-4aa7-ba81-a14a0cf4b306\"}\n[2026-05-11 10:42:36] local.INFO: [EmailSchedule] STARTING batch create {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"4243f43b-9d70-4dd0-9c98-3122eb9445ed\",\"trace_id\":\"0c811e1d-ed7e-4aa7-ba81-a14a0cf4b306\"}\n[2026-05-11 10:42:36] local.INFO: [EmailSchedule] FINISHED batch create {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"4243f43b-9d70-4dd0-9c98-3122eb9445ed\",\"trace_id\":\"0c811e1d-ed7e-4aa7-ba81-a14a0cf4b306\"}\n[2026-05-11 10:42:36] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:create\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":60.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"4243f43b-9d70-4dd0-9c98-3122eb9445ed\",\"trace_id\":\"0c811e1d-ed7e-4aa7-ba81-a14a0cf4b306\"}\n[2026-05-11 10:42:39] local.INFO: [Jiminny\\Jobs\\Mailbox\\CreateBatches] processed 2 inboxes and created 0 batches {\"userId\":null,\"batchSize\":30,\"maxBatches\":1000} {\"correlation_id\":\"2328b74c-d9ea-43ef-95db-20ea7c6ec9f5\",\"trace_id\":\"0c811e1d-ed7e-4aa7-ba81-a14a0cf4b306\"}\n[2026-05-11 10:42:54] local.INFO: [HubSpot Webhook] Signature validation started {\"method\":\"POST\",\"uri\":\"/webhook/conference/hubspot/events\",\"has_v3_signature\":true,\"has_v1_signature\":true} {\"correlation_id\":\"35391537-e747-4658-94f7-9d3d8c65eab6\",\"trace_id\":\"163e44b3-9f8e-4957-b04d-4937c33d30e6\"}\n[2026-05-11 10:42:54] local.INFO: [HubSpot Webhook] Event received {\"payload\":[{\"eventId\":1531490889,\"subscriptionId\":5683273,\"portalId\":4392066,\"appId\":38482,\"occurredAt\":1778478181931,\"subscriptionType\":\"company.associationChange\",\"attemptNumber\":8,\"changeSource\":\"USER\",\"associationType\":\"COMPANY_TO_DEAL\",\"fromObjectId\":13266161465,\"toObjectId\":60142058500,\"associationRemoved\":false,\"isPrimaryAssociation\":false}],\"event_count\":1} {\"correlation_id\":\"35391537-e747-4658-94f7-9d3d8c65eab6\",\"trace_id\":\"163e44b3-9f8e-4957-b04d-4937c33d30e6\"}\n[2026-05-11 10:42:54] local.INFO: [HubSpot Webhook Job] Processing webhook events {\"event_count\":1,\"local_count\":1,\"forward_count\":1} {\"correlation_id\":\"bb5ee15c-0826-40ef-a4c4-83761c307c7b\",\"trace_id\":\"163e44b3-9f8e-4957-b04d-4937c33d30e6\"}\n[2026-05-11 10:42:55] local.INFO: [Webhook Forwarder] Forwarding to instance {\"url\":\"https://uranus.staging.jiminny.com/internal/webhook-receiver/hubspot\",\"route\":\"internal.webhook-receiver.hubspot\",\"event_count\":1} {\"correlation_id\":\"bb5ee15c-0826-40ef-a4c4-83761c307c7b\",\"trace_id\":\"163e44b3-9f8e-4957-b04d-4937c33d30e6\"}\n[2026-05-11 10:42:55] local.ERROR: [Webhook Forwarder] Failed to forward to instance {\"url\":\"https://uranus.staging.jiminny.com\",\"route\":\"internal.webhook-receiver.hubspot\",\"error\":\"Server error: `POST https://uranus.staging.jiminny.com/internal/webhook-receiver/hubspot` resulted in a `503 Service Temporarily Unavailable` response:\n<html>\n\n<head><title>503 Service Temporarily Unavailable</title></head>\n\n<body>\n\n<center><h1>503 Service Temporarily Una (truncated...)\n\",\"code\":503} {\"correlation_id\":\"bb5ee15c-0826-40ef-a4c4-83761c307c7b\",\"trace_id\":\"163e44b3-9f8e-4957-b04d-4937c33d30e6\"}\n[2026-05-11 10:42:55] local.INFO: [Webhook Forwarder] Forwarding to instance {\"url\":\"https://app.qai.jiminny.com/internal/webhook-receiver/hubspot\",\"route\":\"internal.webhook-receiver.hubspot\",\"event_count\":1} {\"correlation_id\":\"bb5ee15c-0826-40ef-a4c4-83761c307c7b\",\"trace_id\":\"163e44b3-9f8e-4957-b04d-4937c33d30e6\"}\n[2026-05-11 10:42:56] local.INFO: [Webhook Forwarder] Successfully forwarded to instance {\"url\":\"https://app.qai.jiminny.com/internal/webhook-receiver/hubspot\",\"status_code\":202,\"response\":{\"status\":\"accepted\",\"message\":\"Events queued for processing\",\"event_count\":1}} {\"correlation_id\":\"bb5ee15c-0826-40ef-a4c4-83761c307c7b\",\"trace_id\":\"163e44b3-9f8e-4957-b04d-4937c33d30e6\"}\n[2026-05-11 10:42:56] local.INFO: [BatchSyncCollector] Stored webhook in Redis {\"object_type\":\"company\",\"crm_provider_id\":\"13266161465\",\"event_type\":\"association_change\",\"configuration_id\":2,\"batch_key\":\"batch_sync_company:ids:2:all\",\"was_new\":true,\"current_size\":1} {\"correlation_id\":\"bb5ee15c-0826-40ef-a4c4-83761c307c7b\",\"trace_id\":\"163e44b3-9f8e-4957-b04d-4937c33d30e6\"}\n[2026-05-11 10:43:14] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"a2a4326d-0416-4ea7-8eac-370e0fee93fb\",\"trace_id\":\"49cf2405-4c7b-48ce-9aac-074f08037e94\"}\n[2026-05-11 10:43:14] local.INFO: [ScheduleBotCommand] Number of activities to be captured: 0 {\"correlation_id\":\"a2a4326d-0416-4ea7-8eac-370e0fee93fb\",\"trace_id\":\"49cf2405-4c7b-48ce-9aac-074f08037e94\"}\n[2026-05-11 10:43:14] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"meeting-bot:schedule-bot\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":60.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"a2a4326d-0416-4ea7-8eac-370e0fee93fb\",\"trace_id\":\"49cf2405-4c7b-48ce-9aac-074f08037e94\"}\n[2026-05-11 10:43:18] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":615092,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"e7be542b-ee51-44b8-a563-a2eac45b3413\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:18] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":615092} {\"correlation_id\":\"e7be542b-ee51-44b8-a563-a2eac45b3413\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:18] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":615092,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"e7be542b-ee51-44b8-a563-a2eac45b3413\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:18] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":615092,\"participants\":[{\"id\":1004102,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":1004103,\"user_id\":89,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"e7be542b-ee51-44b8-a563-a2eac45b3413\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:19] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"e7be542b-ee51-44b8-a563-a2eac45b3413\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:19] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"e7be542b-ee51-44b8-a563-a2eac45b3413\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:19] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"e7be542b-ee51-44b8-a563-a2eac45b3413\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:19] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"e7be542b-ee51-44b8-a563-a2eac45b3413\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:20] local.INFO: [Prospect match] Cache miss, calling the API {\"identifier_type\":\"email\",\"identifier\":\"nikolay.nikolov@jiminny.com\"} {\"correlation_id\":\"e7be542b-ee51-44b8-a563-a2eac45b3413\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:20] local.INFO: [Hubspot] Failed to fetch contact {\"email\":\"nikolay.nikolov@jiminny.com\",\"reason\":\"[404] Client error: `GET https://api.hubapi.com/crm/v3/objects/contacts/nikolay.nikolov%40jiminny.com?properties=email%2Cfirstname%2Clastname%2Ccountry%2Cphone%2Cmobilephone%2Cjobtitle%2Chubspot_owner_id%2Cassociatedcompanyid%2Cphoto&archived=0&idProperty=email` resulted in a `404 Not Found` response\"} {\"correlation_id\":\"e7be542b-ee51-44b8-a563-a2eac45b3413\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:20] local.INFO: [Prospect match] API returned empty result, caching the miss with empty prospect data {\"identifier_type\":\"email\",\"identifier\":\"nikolay.nikolov@jiminny.com\"} {\"correlation_id\":\"e7be542b-ee51-44b8-a563-a2eac45b3413\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:21] local.INFO: [Hubspot] Pagination completed {\"team_id\":2,\"endpoint\":\"https://api.hubapi.com/crm/v3/objects/contact/search\",\"total_requests\":1,\"total_records_fetched\":0,\"total_elapsed_seconds\":0.38,\"average_seconds_per_request\":0.38} {\"correlation_id\":\"e7be542b-ee51-44b8-a563-a2eac45b3413\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:22] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"ab7124d8-7931-41ef-8341-93a3305f65eb\",\"trace_id\":\"4564d24d-bf9e-486b-ac8e-0f69c9b286d8\"}\n[2026-05-11 10:43:22] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"dialers:monitor-activities\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":60.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"ab7124d8-7931-41ef-8341-93a3305f65eb\",\"trace_id\":\"4564d24d-bf9e-486b-ac8e-0f69c9b286d8\"}\n[2026-05-11 10:43:22] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"nikolay.nikolov@jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"e7be542b-ee51-44b8-a563-a2eac45b3413\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:22] local.INFO: [Prospect match] Cache miss {\"identifier_type\":\"domain\",\"identifier\":\"jiminny.com\",\"crm\":\"hubspot\"} {\"correlation_id\":\"e7be542b-ee51-44b8-a563-a2eac45b3413\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:22] local.INFO: [Prospect match] Cache miss, calling the API {\"identifier_type\":\"domain\",\"identifier\":\"jiminny.com\"} {\"correlation_id\":\"e7be542b-ee51-44b8-a563-a2eac45b3413\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:23] local.INFO: [HubSpot] importAccount {\"crm_provider_id\":\"749766179\",\"config_id\":2} {\"correlation_id\":\"e7be542b-ee51-44b8-a563-a2eac45b3413\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:23] local.INFO: [HubSpot] CRM Search requested {\"request\":{\"filterGroups\":[{\"filters\":[{\"propertyName\":\"associations.company\",\"operator\":\"EQ\",\"value\":\"749766179\"},{\"propertyName\":\"dealstage\",\"operator\":\"NOT_IN\",\"values\":[\"closedwon\",\"4040964\",\"59247967\"]},{\"propertyName\":\"dealstage\",\"operator\":\"NOT_IN\",\"values\":[\"closedlost\",\"4040965\",\"59247968\"]}]}],\"sorts\":[{\"propertyName\":\"modifieddate\",\"direction\":\"DESCENDING\"}],\"properties\":[\"dealname\",\"amount\",\"hubspot_owner_id\",\"pipeline\",\"dealstage\",\"closedate\",\"deal_currency_code\"],\"limit\":200}} {\"correlation_id\":\"e7be542b-ee51-44b8-a563-a2eac45b3413\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:23] local.INFO: [Hubspot] Pagination completed {\"team_id\":2,\"endpoint\":\"https://api.hubapi.com/crm/v3/objects/deals/search\",\"total_requests\":1,\"total_records_fetched\":10,\"total_elapsed_seconds\":0.3,\"average_seconds_per_request\":0.3} {\"correlation_id\":\"e7be542b-ee51-44b8-a563-a2eac45b3413\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:24] local.INFO: [ EsUpdateProcessManager ] Finished updating entities in ES {\"worker\":\"\",\"peak_memory\":\"99.73 MB\",\"elapsed_seconds\":3.22,\"update_target\":\"activities\",\"should_iterate_again\":false} {\"correlation_id\":\"4ad05333-9afb-492e-9f0f-b2909ac45b32\",\"trace_id\":\"3d8feb24-b173-4158-b0a4-4cf33af85066\"}\n[2026-05-11 10:43:24] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":615092,\"team_id\":2,\"email\":\"nikolay.nikolov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"e7be542b-ee51-44b8-a563-a2eac45b3413\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:24] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":615092,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"e7be542b-ee51-44b8-a563-a2eac45b3413\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:24] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":615092} {\"correlation_id\":\"e7be542b-ee51-44b8-a563-a2eac45b3413\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:24] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":615092,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"e7be542b-ee51-44b8-a563-a2eac45b3413\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:24] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":615092,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"e7be542b-ee51-44b8-a563-a2eac45b3413\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:24] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":614436,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"a34067d1-038f-48bf-af25-c9e09374ce62\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:24] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":614436} {\"correlation_id\":\"a34067d1-038f-48bf-af25-c9e09374ce62\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:24] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":614436,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"a34067d1-038f-48bf-af25-c9e09374ce62\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:24] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":614436,\"participants\":[{\"id\":1002751,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":1002752,\"user_id\":89,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"a34067d1-038f-48bf-af25-c9e09374ce62\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:24] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"a34067d1-038f-48bf-af25-c9e09374ce62\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:24] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"a34067d1-038f-48bf-af25-c9e09374ce62\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:24] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"a34067d1-038f-48bf-af25-c9e09374ce62\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:24] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"a34067d1-038f-48bf-af25-c9e09374ce62\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:24] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"nikolay.nikolov@jiminny.com\"} {\"correlation_id\":\"a34067d1-038f-48bf-af25-c9e09374ce62\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:24] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"nikolay.nikolov@jiminny.com\"} {\"correlation_id\":\"a34067d1-038f-48bf-af25-c9e09374ce62\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:24] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"nikolay.nikolov@jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"a34067d1-038f-48bf-af25-c9e09374ce62\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:24] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":614436,\"team_id\":2,\"email\":\"nikolay.nikolov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"a34067d1-038f-48bf-af25-c9e09374ce62\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:24] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":614436,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"a34067d1-038f-48bf-af25-c9e09374ce62\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:24] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":614436} {\"correlation_id\":\"a34067d1-038f-48bf-af25-c9e09374ce62\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:24] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":614436,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"a34067d1-038f-48bf-af25-c9e09374ce62\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:24] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":614436,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"a34067d1-038f-48bf-af25-c9e09374ce62\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":614382,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"ee88af0b-20d3-4911-a378-bea7fc6dc3f6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":614382} {\"correlation_id\":\"ee88af0b-20d3-4911-a378-bea7fc6dc3f6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":614382,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"ee88af0b-20d3-4911-a378-bea7fc6dc3f6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":614382,\"participants\":[{\"id\":1002632,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":1002633,\"user_id\":89,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"ee88af0b-20d3-4911-a378-bea7fc6dc3f6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"ee88af0b-20d3-4911-a378-bea7fc6dc3f6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"ee88af0b-20d3-4911-a378-bea7fc6dc3f6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"ee88af0b-20d3-4911-a378-bea7fc6dc3f6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"ee88af0b-20d3-4911-a378-bea7fc6dc3f6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"nikolay.nikolov@jiminny.com\"} {\"correlation_id\":\"ee88af0b-20d3-4911-a378-bea7fc6dc3f6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"nikolay.nikolov@jiminny.com\"} {\"correlation_id\":\"ee88af0b-20d3-4911-a378-bea7fc6dc3f6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"nikolay.nikolov@jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"ee88af0b-20d3-4911-a378-bea7fc6dc3f6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":614382,\"team_id\":2,\"email\":\"nikolay.nikolov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"ee88af0b-20d3-4911-a378-bea7fc6dc3f6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":614382,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"ee88af0b-20d3-4911-a378-bea7fc6dc3f6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":614382} {\"correlation_id\":\"ee88af0b-20d3-4911-a378-bea7fc6dc3f6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":614382,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"ee88af0b-20d3-4911-a378-bea7fc6dc3f6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":614382,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"ee88af0b-20d3-4911-a378-bea7fc6dc3f6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":614381,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"535b99f0-d38e-4b9c-bf19-3751ce4e6a1d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":614381} {\"correlation_id\":\"535b99f0-d38e-4b9c-bf19-3751ce4e6a1d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":614381,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"535b99f0-d38e-4b9c-bf19-3751ce4e6a1d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":614381,\"participants\":[{\"id\":1002630,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":1002631,\"user_id\":89,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"535b99f0-d38e-4b9c-bf19-3751ce4e6a1d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"535b99f0-d38e-4b9c-bf19-3751ce4e6a1d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"535b99f0-d38e-4b9c-bf19-3751ce4e6a1d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"535b99f0-d38e-4b9c-bf19-3751ce4e6a1d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"535b99f0-d38e-4b9c-bf19-3751ce4e6a1d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"nikolay.nikolov@jiminny.com\"} {\"correlation_id\":\"535b99f0-d38e-4b9c-bf19-3751ce4e6a1d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"nikolay.nikolov@jiminny.com\"} {\"correlation_id\":\"535b99f0-d38e-4b9c-bf19-3751ce4e6a1d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"nikolay.nikolov@jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"535b99f0-d38e-4b9c-bf19-3751ce4e6a1d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":614381,\"team_id\":2,\"email\":\"nikolay.nikolov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"535b99f0-d38e-4b9c-bf19-3751ce4e6a1d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":614381,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"535b99f0-d38e-4b9c-bf19-3751ce4e6a1d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":614381} {\"correlation_id\":\"535b99f0-d38e-4b9c-bf19-3751ce4e6a1d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":614381,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"535b99f0-d38e-4b9c-bf19-3751ce4e6a1d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":614381,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"535b99f0-d38e-4b9c-bf19-3751ce4e6a1d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":614378,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":6167,\"account_id\":null,\"opportunity_id\":null,\"stage_id\":null}} {\"correlation_id\":\"f934b469-16df-4a4b-b5cc-bf918da87b87\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":614378} {\"correlation_id\":\"f934b469-16df-4a4b-b5cc-bf918da87b87\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":614378,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"f934b469-16df-4a4b-b5cc-bf918da87b87\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":614378,\"participants\":[{\"id\":1002623,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":1002624,\"user_id\":null,\"contact_id\":6167,\"lead_id\":null},{\"id\":1002625,\"user_id\":89,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"f934b469-16df-4a4b-b5cc-bf918da87b87\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"f934b469-16df-4a4b-b5cc-bf918da87b87\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"f934b469-16df-4a4b-b5cc-bf918da87b87\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"f934b469-16df-4a4b-b5cc-bf918da87b87\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"f934b469-16df-4a4b-b5cc-bf918da87b87\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"nikolay.nikolov@jiminny.com\"} {\"correlation_id\":\"f934b469-16df-4a4b-b5cc-bf918da87b87\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"nikolay.nikolov@jiminny.com\"} {\"correlation_id\":\"f934b469-16df-4a4b-b5cc-bf918da87b87\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"nikolay.nikolov@jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"f934b469-16df-4a4b-b5cc-bf918da87b87\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"nmalchev@gmail.com\"} {\"correlation_id\":\"f934b469-16df-4a4b-b5cc-bf918da87b87\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":614378,\"team_id\":2,\"email\":\"nikolay.nikolov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"f934b469-16df-4a4b-b5cc-bf918da87b87\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":614378,\"participants_processed\":3,\"exact_matches\":1,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"f934b469-16df-4a4b-b5cc-bf918da87b87\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":614378} {\"correlation_id\":\"f934b469-16df-4a4b-b5cc-bf918da87b87\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":614378,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"f934b469-16df-4a4b-b5cc-bf918da87b87\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":614378,\"remote_search\":true,\"lead_id\":null,\"contact_id\":6167,\"account_id\":null,\"opportunity_id\":null,\"stage_id\":null} {\"correlation_id\":\"f934b469-16df-4a4b-b5cc-bf918da87b87\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":613840,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36}} {\"correlation_id\":\"b440c986-63bd-445c-adfc-4d111801e63c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613840} {\"correlation_id\":\"b440c986-63bd-445c-adfc-4d111801e63c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613840,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"b440c986-63bd-445c-adfc-4d111801e63c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":613840,\"participants\":[{\"id\":1001764,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null},{\"id\":1001765,\"user_id\":261,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"b440c986-63bd-445c-adfc-4d111801e63c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"b440c986-63bd-445c-adfc-4d111801e63c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"b440c986-63bd-445c-adfc-4d111801e63c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"b440c986-63bd-445c-adfc-4d111801e63c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"b440c986-63bd-445c-adfc-4d111801e63c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: ProspectCache - Searching DB for opportunity by owner {\"account_id\":244,\"contact_id\":4487,\"owner_id\":261} {\"correlation_id\":\"b440c986-63bd-445c-adfc-4d111801e63c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: ProspectCache - Opportunity DB search results {\"account_id\":244,\"contact_id\":4487,\"opportunity_id\":299} {\"correlation_id\":\"b440c986-63bd-445c-adfc-4d111801e63c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"b440c986-63bd-445c-adfc-4d111801e63c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":613840,\"team_id\":2,\"email\":\"adelina.petrova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"b440c986-63bd-445c-adfc-4d111801e63c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":613840,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"b440c986-63bd-445c-adfc-4d111801e63c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613840} {\"correlation_id\":\"b440c986-63bd-445c-adfc-4d111801e63c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613840,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"b440c986-63bd-445c-adfc-4d111801e63c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":613840,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36} {\"correlation_id\":\"b440c986-63bd-445c-adfc-4d111801e63c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":613833,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36}} {\"correlation_id\":\"0360a22a-94dd-43cb-b57b-06bc159a05ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613833} {\"correlation_id\":\"0360a22a-94dd-43cb-b57b-06bc159a05ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613833,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"0360a22a-94dd-43cb-b57b-06bc159a05ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":613833,\"participants\":[{\"id\":1001750,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null},{\"id\":1001751,\"user_id\":261,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"0360a22a-94dd-43cb-b57b-06bc159a05ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"0360a22a-94dd-43cb-b57b-06bc159a05ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"0360a22a-94dd-43cb-b57b-06bc159a05ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"0360a22a-94dd-43cb-b57b-06bc159a05ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"0360a22a-94dd-43cb-b57b-06bc159a05ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"0360a22a-94dd-43cb-b57b-06bc159a05ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":613833,\"team_id\":2,\"email\":\"adelina.petrova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"0360a22a-94dd-43cb-b57b-06bc159a05ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":613833,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"0360a22a-94dd-43cb-b57b-06bc159a05ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613833} {\"correlation_id\":\"0360a22a-94dd-43cb-b57b-06bc159a05ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613833,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"0360a22a-94dd-43cb-b57b-06bc159a05ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":613833,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36} {\"correlation_id\":\"0360a22a-94dd-43cb-b57b-06bc159a05ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":613827,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36}} {\"correlation_id\":\"32fbe130-bf29-4e37-9c5a-bbe81f416a36\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613827} {\"correlation_id\":\"32fbe130-bf29-4e37-9c5a-bbe81f416a36\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613827,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"32fbe130-bf29-4e37-9c5a-bbe81f416a36\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":613827,\"participants\":[{\"id\":1001734,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null},{\"id\":1001735,\"user_id\":261,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"32fbe130-bf29-4e37-9c5a-bbe81f416a36\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"32fbe130-bf29-4e37-9c5a-bbe81f416a36\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"32fbe130-bf29-4e37-9c5a-bbe81f416a36\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"32fbe130-bf29-4e37-9c5a-bbe81f416a36\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"32fbe130-bf29-4e37-9c5a-bbe81f416a36\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"32fbe130-bf29-4e37-9c5a-bbe81f416a36\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":613827,\"team_id\":2,\"email\":\"adelina.petrova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"32fbe130-bf29-4e37-9c5a-bbe81f416a36\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":613827,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"32fbe130-bf29-4e37-9c5a-bbe81f416a36\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613827} {\"correlation_id\":\"32fbe130-bf29-4e37-9c5a-bbe81f416a36\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613827,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"32fbe130-bf29-4e37-9c5a-bbe81f416a36\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":613827,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36} {\"correlation_id\":\"32fbe130-bf29-4e37-9c5a-bbe81f416a36\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":613826,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36}} {\"correlation_id\":\"9b9716ff-ede9-4377-8ee8-b3c9a68b2dfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613826} {\"correlation_id\":\"9b9716ff-ede9-4377-8ee8-b3c9a68b2dfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613826,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"9b9716ff-ede9-4377-8ee8-b3c9a68b2dfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":613826,\"participants\":[{\"id\":1001732,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null},{\"id\":1001733,\"user_id\":261,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"9b9716ff-ede9-4377-8ee8-b3c9a68b2dfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"9b9716ff-ede9-4377-8ee8-b3c9a68b2dfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"9b9716ff-ede9-4377-8ee8-b3c9a68b2dfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"9b9716ff-ede9-4377-8ee8-b3c9a68b2dfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"9b9716ff-ede9-4377-8ee8-b3c9a68b2dfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"9b9716ff-ede9-4377-8ee8-b3c9a68b2dfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":613826,\"team_id\":2,\"email\":\"adelina.petrova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"9b9716ff-ede9-4377-8ee8-b3c9a68b2dfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":613826,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"9b9716ff-ede9-4377-8ee8-b3c9a68b2dfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613826} {\"correlation_id\":\"9b9716ff-ede9-4377-8ee8-b3c9a68b2dfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613826,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"9b9716ff-ede9-4377-8ee8-b3c9a68b2dfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":613826,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36} {\"correlation_id\":\"9b9716ff-ede9-4377-8ee8-b3c9a68b2dfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":613820,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36}} {\"correlation_id\":\"a131075c-6ae7-4912-92e1-adc33c291d1e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613820} {\"correlation_id\":\"a131075c-6ae7-4912-92e1-adc33c291d1e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613820,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"a131075c-6ae7-4912-92e1-adc33c291d1e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":613820,\"participants\":[{\"id\":1001721,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null},{\"id\":1001722,\"user_id\":261,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"a131075c-6ae7-4912-92e1-adc33c291d1e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"a131075c-6ae7-4912-92e1-adc33c291d1e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"a131075c-6ae7-4912-92e1-adc33c291d1e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"a131075c-6ae7-4912-92e1-adc33c291d1e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"a131075c-6ae7-4912-92e1-adc33c291d1e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"a131075c-6ae7-4912-92e1-adc33c291d1e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":613820,\"team_id\":2,\"email\":\"adelina.petrova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"a131075c-6ae7-4912-92e1-adc33c291d1e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":613820,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"a131075c-6ae7-4912-92e1-adc33c291d1e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613820} {\"correlation_id\":\"a131075c-6ae7-4912-92e1-adc33c291d1e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613820,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"a131075c-6ae7-4912-92e1-adc33c291d1e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":613820,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36} {\"correlation_id\":\"a131075c-6ae7-4912-92e1-adc33c291d1e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":613818,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36}} {\"correlation_id\":\"d36e09b8-5b15-40f2-8fa0-0beb3f0ebe69\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613818} {\"correlation_id\":\"d36e09b8-5b15-40f2-8fa0-0beb3f0ebe69\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613818,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"d36e09b8-5b15-40f2-8fa0-0beb3f0ebe69\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":613818,\"participants\":[{\"id\":1001717,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null},{\"id\":1001718,\"user_id\":261,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"d36e09b8-5b15-40f2-8fa0-0beb3f0ebe69\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"d36e09b8-5b15-40f2-8fa0-0beb3f0ebe69\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"d36e09b8-5b15-40f2-8fa0-0beb3f0ebe69\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d36e09b8-5b15-40f2-8fa0-0beb3f0ebe69\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"d36e09b8-5b15-40f2-8fa0-0beb3f0ebe69\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"d36e09b8-5b15-40f2-8fa0-0beb3f0ebe69\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":613818,\"team_id\":2,\"email\":\"adelina.petrova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"d36e09b8-5b15-40f2-8fa0-0beb3f0ebe69\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":613818,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"d36e09b8-5b15-40f2-8fa0-0beb3f0ebe69\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613818} {\"correlation_id\":\"d36e09b8-5b15-40f2-8fa0-0beb3f0ebe69\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613818,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"d36e09b8-5b15-40f2-8fa0-0beb3f0ebe69\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":613818,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36} {\"correlation_id\":\"d36e09b8-5b15-40f2-8fa0-0beb3f0ebe69\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":613812,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36}} {\"correlation_id\":\"9e5ea20b-def9-4ace-8a73-9d08a04cc6d9\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613812} {\"correlation_id\":\"9e5ea20b-def9-4ace-8a73-9d08a04cc6d9\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613812,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"9e5ea20b-def9-4ace-8a73-9d08a04cc6d9\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":613812,\"participants\":[{\"id\":1001705,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null},{\"id\":1001706,\"user_id\":261,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"9e5ea20b-def9-4ace-8a73-9d08a04cc6d9\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"9e5ea20b-def9-4ace-8a73-9d08a04cc6d9\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"9e5ea20b-def9-4ace-8a73-9d08a04cc6d9\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"9e5ea20b-def9-4ace-8a73-9d08a04cc6d9\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"9e5ea20b-def9-4ace-8a73-9d08a04cc6d9\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"9e5ea20b-def9-4ace-8a73-9d08a04cc6d9\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":613812,\"team_id\":2,\"email\":\"adelina.petrova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"9e5ea20b-def9-4ace-8a73-9d08a04cc6d9\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":613812,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"9e5ea20b-def9-4ace-8a73-9d08a04cc6d9\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613812} {\"correlation_id\":\"9e5ea20b-def9-4ace-8a73-9d08a04cc6d9\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613812,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"9e5ea20b-def9-4ace-8a73-9d08a04cc6d9\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":613812,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36} {\"correlation_id\":\"9e5ea20b-def9-4ace-8a73-9d08a04cc6d9\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":613807,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4484,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"be53e876-a319-40dc-a89b-0728d9a47c6e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613807} {\"correlation_id\":\"be53e876-a319-40dc-a89b-0728d9a47c6e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613807,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"be53e876-a319-40dc-a89b-0728d9a47c6e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":613807,\"participants\":[{\"id\":1001690,\"user_id\":253,\"contact_id\":null,\"lead_id\":null},{\"id\":1001691,\"user_id\":null,\"contact_id\":4484,\"lead_id\":null}]} {\"correlation_id\":\"be53e876-a319-40dc-a89b-0728d9a47c6e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"be53e876-a319-40dc-a89b-0728d9a47c6e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"be53e876-a319-40dc-a89b-0728d9a47c6e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"be53e876-a319-40dc-a89b-0728d9a47c6e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"be53e876-a319-40dc-a89b-0728d9a47c6e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":613807,\"team_id\":2,\"email\":\"preslava.ivanova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"be53e876-a319-40dc-a89b-0728d9a47c6e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: ProspectCache - Searching DB for opportunity by owner {\"account_id\":243,\"contact_id\":4484,\"owner_id\":253} {\"correlation_id\":\"be53e876-a319-40dc-a89b-0728d9a47c6e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: ProspectCache - Fallback DB opportunity search {\"account_id\":243,\"contact_id\":4484} {\"correlation_id\":\"be53e876-a319-40dc-a89b-0728d9a47c6e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: ProspectCache - Opportunity DB search results {\"account_id\":243,\"contact_id\":4484,\"opportunity_id\":276} {\"correlation_id\":\"be53e876-a319-40dc-a89b-0728d9a47c6e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"tsvetomir.banovski@gmail.com\"} {\"correlation_id\":\"be53e876-a319-40dc-a89b-0728d9a47c6e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":613807,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"be53e876-a319-40dc-a89b-0728d9a47c6e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613807} {\"correlation_id\":\"be53e876-a319-40dc-a89b-0728d9a47c6e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613807,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"be53e876-a319-40dc-a89b-0728d9a47c6e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":613807,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4484,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"be53e876-a319-40dc-a89b-0728d9a47c6e\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":613806,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":350,\"stage_id\":34}} {\"correlation_id\":\"94c53200-4779-41ab-82ea-571c1bcdd8b7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613806} {\"correlation_id\":\"94c53200-4779-41ab-82ea-571c1bcdd8b7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613806,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"94c53200-4779-41ab-82ea-571c1bcdd8b7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":613806,\"participants\":[{\"id\":1001688,\"user_id\":253,\"contact_id\":null,\"lead_id\":null},{\"id\":1001689,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null}]} {\"correlation_id\":\"94c53200-4779-41ab-82ea-571c1bcdd8b7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"94c53200-4779-41ab-82ea-571c1bcdd8b7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"94c53200-4779-41ab-82ea-571c1bcdd8b7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"94c53200-4779-41ab-82ea-571c1bcdd8b7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"94c53200-4779-41ab-82ea-571c1bcdd8b7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":613806,\"team_id\":2,\"email\":\"preslava.ivanova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"94c53200-4779-41ab-82ea-571c1bcdd8b7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: ProspectCache - Searching DB for opportunity by owner {\"account_id\":244,\"contact_id\":4487,\"owner_id\":253} {\"correlation_id\":\"94c53200-4779-41ab-82ea-571c1bcdd8b7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: ProspectCache - Fallback DB opportunity search {\"account_id\":244,\"contact_id\":4487} {\"correlation_id\":\"94c53200-4779-41ab-82ea-571c1bcdd8b7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: ProspectCache - Opportunity DB search results {\"account_id\":244,\"contact_id\":4487,\"opportunity_id\":350} {\"correlation_id\":\"94c53200-4779-41ab-82ea-571c1bcdd8b7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"94c53200-4779-41ab-82ea-571c1bcdd8b7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":613806,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"94c53200-4779-41ab-82ea-571c1bcdd8b7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613806} {\"correlation_id\":\"94c53200-4779-41ab-82ea-571c1bcdd8b7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613806,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"94c53200-4779-41ab-82ea-571c1bcdd8b7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":613806,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":350,\"stage_id\":34} {\"correlation_id\":\"94c53200-4779-41ab-82ea-571c1bcdd8b7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":613805,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":350,\"stage_id\":34}} {\"correlation_id\":\"d1a190ef-9b03-49ff-ad47-50afa53ef345\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613805} {\"correlation_id\":\"d1a190ef-9b03-49ff-ad47-50afa53ef345\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613805,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"d1a190ef-9b03-49ff-ad47-50afa53ef345\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":613805,\"participants\":[{\"id\":1001686,\"user_id\":253,\"contact_id\":null,\"lead_id\":null},{\"id\":1001687,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null}]} {\"correlation_id\":\"d1a190ef-9b03-49ff-ad47-50afa53ef345\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"d1a190ef-9b03-49ff-ad47-50afa53ef345\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"d1a190ef-9b03-49ff-ad47-50afa53ef345\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d1a190ef-9b03-49ff-ad47-50afa53ef345\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"d1a190ef-9b03-49ff-ad47-50afa53ef345\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":613805,\"team_id\":2,\"email\":\"preslava.ivanova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"d1a190ef-9b03-49ff-ad47-50afa53ef345\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"d1a190ef-9b03-49ff-ad47-50afa53ef345\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":613805,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"d1a190ef-9b03-49ff-ad47-50afa53ef345\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613805} {\"correlation_id\":\"d1a190ef-9b03-49ff-ad47-50afa53ef345\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613805,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"d1a190ef-9b03-49ff-ad47-50afa53ef345\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:25] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":613805,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":350,\"stage_id\":34} {\"correlation_id\":\"d1a190ef-9b03-49ff-ad47-50afa53ef345\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":613698,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36}} {\"correlation_id\":\"2fa5fec1-54fb-40cc-93d4-145712a9d2fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613698} {\"correlation_id\":\"2fa5fec1-54fb-40cc-93d4-145712a9d2fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613698,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"2fa5fec1-54fb-40cc-93d4-145712a9d2fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":613698,\"participants\":[{\"id\":1001667,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null},{\"id\":1001668,\"user_id\":261,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"2fa5fec1-54fb-40cc-93d4-145712a9d2fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"2fa5fec1-54fb-40cc-93d4-145712a9d2fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"2fa5fec1-54fb-40cc-93d4-145712a9d2fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"2fa5fec1-54fb-40cc-93d4-145712a9d2fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"2fa5fec1-54fb-40cc-93d4-145712a9d2fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"2fa5fec1-54fb-40cc-93d4-145712a9d2fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":613698,\"team_id\":2,\"email\":\"adelina.petrova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"2fa5fec1-54fb-40cc-93d4-145712a9d2fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":613698,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"2fa5fec1-54fb-40cc-93d4-145712a9d2fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613698} {\"correlation_id\":\"2fa5fec1-54fb-40cc-93d4-145712a9d2fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613698,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"2fa5fec1-54fb-40cc-93d4-145712a9d2fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":613698,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36} {\"correlation_id\":\"2fa5fec1-54fb-40cc-93d4-145712a9d2fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":613697,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36}} {\"correlation_id\":\"4da249c2-1056-47f9-869a-53c5fda5b9ef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613697} {\"correlation_id\":\"4da249c2-1056-47f9-869a-53c5fda5b9ef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613697,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"4da249c2-1056-47f9-869a-53c5fda5b9ef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":613697,\"participants\":[{\"id\":1001665,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null},{\"id\":1001666,\"user_id\":261,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"4da249c2-1056-47f9-869a-53c5fda5b9ef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"4da249c2-1056-47f9-869a-53c5fda5b9ef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"4da249c2-1056-47f9-869a-53c5fda5b9ef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4da249c2-1056-47f9-869a-53c5fda5b9ef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"4da249c2-1056-47f9-869a-53c5fda5b9ef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"4da249c2-1056-47f9-869a-53c5fda5b9ef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":613697,\"team_id\":2,\"email\":\"adelina.petrova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"4da249c2-1056-47f9-869a-53c5fda5b9ef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":613697,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"4da249c2-1056-47f9-869a-53c5fda5b9ef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613697} {\"correlation_id\":\"4da249c2-1056-47f9-869a-53c5fda5b9ef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613697,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"4da249c2-1056-47f9-869a-53c5fda5b9ef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":613697,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36} {\"correlation_id\":\"4da249c2-1056-47f9-869a-53c5fda5b9ef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":613696,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36}} {\"correlation_id\":\"8224abca-8e96-4a39-8baa-b4bc4f746e1b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613696} {\"correlation_id\":\"8224abca-8e96-4a39-8baa-b4bc4f746e1b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613696,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"8224abca-8e96-4a39-8baa-b4bc4f746e1b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":613696,\"participants\":[{\"id\":1001663,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null},{\"id\":1001664,\"user_id\":261,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"8224abca-8e96-4a39-8baa-b4bc4f746e1b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"8224abca-8e96-4a39-8baa-b4bc4f746e1b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"8224abca-8e96-4a39-8baa-b4bc4f746e1b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"8224abca-8e96-4a39-8baa-b4bc4f746e1b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"8224abca-8e96-4a39-8baa-b4bc4f746e1b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"8224abca-8e96-4a39-8baa-b4bc4f746e1b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":613696,\"team_id\":2,\"email\":\"adelina.petrova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"8224abca-8e96-4a39-8baa-b4bc4f746e1b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":613696,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"8224abca-8e96-4a39-8baa-b4bc4f746e1b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613696} {\"correlation_id\":\"8224abca-8e96-4a39-8baa-b4bc4f746e1b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613696,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"8224abca-8e96-4a39-8baa-b4bc4f746e1b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":613696,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36} {\"correlation_id\":\"8224abca-8e96-4a39-8baa-b4bc4f746e1b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":613695,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36}} {\"correlation_id\":\"e6c53892-ee7f-42df-ae80-06e4cab7eeea\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613695} {\"correlation_id\":\"e6c53892-ee7f-42df-ae80-06e4cab7eeea\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613695,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"e6c53892-ee7f-42df-ae80-06e4cab7eeea\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":613695,\"participants\":[{\"id\":1001661,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null},{\"id\":1001662,\"user_id\":261,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"e6c53892-ee7f-42df-ae80-06e4cab7eeea\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"e6c53892-ee7f-42df-ae80-06e4cab7eeea\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"e6c53892-ee7f-42df-ae80-06e4cab7eeea\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"e6c53892-ee7f-42df-ae80-06e4cab7eeea\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"e6c53892-ee7f-42df-ae80-06e4cab7eeea\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"e6c53892-ee7f-42df-ae80-06e4cab7eeea\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":613695,\"team_id\":2,\"email\":\"adelina.petrova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"e6c53892-ee7f-42df-ae80-06e4cab7eeea\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":613695,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"e6c53892-ee7f-42df-ae80-06e4cab7eeea\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613695} {\"correlation_id\":\"e6c53892-ee7f-42df-ae80-06e4cab7eeea\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613695,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"e6c53892-ee7f-42df-ae80-06e4cab7eeea\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":613695,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36} {\"correlation_id\":\"e6c53892-ee7f-42df-ae80-06e4cab7eeea\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.NOTICE: Monitoring start {\"correlation_id\":\"2766b9f0-0c60-4d94-9b69-4f2dc977fe59\",\"trace_id\":\"0810b004-42ab-49a4-b3bf-4fbda4a819fe\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":613694,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36}} {\"correlation_id\":\"34f7f8f5-b1b3-4176-bc4a-f96265db81c5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613694} {\"correlation_id\":\"34f7f8f5-b1b3-4176-bc4a-f96265db81c5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613694,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"34f7f8f5-b1b3-4176-bc4a-f96265db81c5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.NOTICE: Monitoring end {\"correlation_id\":\"2766b9f0-0c60-4d94-9b69-4f2dc977fe59\",\"trace_id\":\"0810b004-42ab-49a4-b3bf-4fbda4a819fe\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":613694,\"participants\":[{\"id\":1001659,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null},{\"id\":1001660,\"user_id\":261,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"34f7f8f5-b1b3-4176-bc4a-f96265db81c5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"34f7f8f5-b1b3-4176-bc4a-f96265db81c5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"34f7f8f5-b1b3-4176-bc4a-f96265db81c5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"34f7f8f5-b1b3-4176-bc4a-f96265db81c5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"34f7f8f5-b1b3-4176-bc4a-f96265db81c5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"34f7f8f5-b1b3-4176-bc4a-f96265db81c5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":613694,\"team_id\":2,\"email\":\"adelina.petrova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"34f7f8f5-b1b3-4176-bc4a-f96265db81c5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":613694,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"34f7f8f5-b1b3-4176-bc4a-f96265db81c5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613694} {\"correlation_id\":\"34f7f8f5-b1b3-4176-bc4a-f96265db81c5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613694,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"34f7f8f5-b1b3-4176-bc4a-f96265db81c5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":613694,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36} {\"correlation_id\":\"34f7f8f5-b1b3-4176-bc4a-f96265db81c5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":613157,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":350,\"stage_id\":34}} {\"correlation_id\":\"38fd2794-89d3-4ce9-a9fc-f9f8382e30d5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613157} {\"correlation_id\":\"38fd2794-89d3-4ce9-a9fc-f9f8382e30d5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613157,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"38fd2794-89d3-4ce9-a9fc-f9f8382e30d5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":613157,\"participants\":[{\"id\":1000746,\"user_id\":253,\"contact_id\":null,\"lead_id\":null},{\"id\":1000747,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null}]} {\"correlation_id\":\"38fd2794-89d3-4ce9-a9fc-f9f8382e30d5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"38fd2794-89d3-4ce9-a9fc-f9f8382e30d5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"38fd2794-89d3-4ce9-a9fc-f9f8382e30d5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"38fd2794-89d3-4ce9-a9fc-f9f8382e30d5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"38fd2794-89d3-4ce9-a9fc-f9f8382e30d5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":613157,\"team_id\":2,\"email\":\"preslava.ivanova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"38fd2794-89d3-4ce9-a9fc-f9f8382e30d5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"38fd2794-89d3-4ce9-a9fc-f9f8382e30d5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":613157,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"38fd2794-89d3-4ce9-a9fc-f9f8382e30d5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613157} {\"correlation_id\":\"38fd2794-89d3-4ce9-a9fc-f9f8382e30d5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613157,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"38fd2794-89d3-4ce9-a9fc-f9f8382e30d5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":613157,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":350,\"stage_id\":34} {\"correlation_id\":\"38fd2794-89d3-4ce9-a9fc-f9f8382e30d5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":613156,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":350,\"stage_id\":34}} {\"correlation_id\":\"f33d171a-96c9-465c-bcf2-facecea0a7d6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613156} {\"correlation_id\":\"f33d171a-96c9-465c-bcf2-facecea0a7d6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613156,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"f33d171a-96c9-465c-bcf2-facecea0a7d6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":613156,\"participants\":[{\"id\":1000744,\"user_id\":253,\"contact_id\":null,\"lead_id\":null},{\"id\":1000745,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null}]} {\"correlation_id\":\"f33d171a-96c9-465c-bcf2-facecea0a7d6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"f33d171a-96c9-465c-bcf2-facecea0a7d6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"f33d171a-96c9-465c-bcf2-facecea0a7d6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"f33d171a-96c9-465c-bcf2-facecea0a7d6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"f33d171a-96c9-465c-bcf2-facecea0a7d6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":613156,\"team_id\":2,\"email\":\"preslava.ivanova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"f33d171a-96c9-465c-bcf2-facecea0a7d6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"f33d171a-96c9-465c-bcf2-facecea0a7d6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":613156,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"f33d171a-96c9-465c-bcf2-facecea0a7d6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613156} {\"correlation_id\":\"f33d171a-96c9-465c-bcf2-facecea0a7d6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613156,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"f33d171a-96c9-465c-bcf2-facecea0a7d6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":613156,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":350,\"stage_id\":34} {\"correlation_id\":\"f33d171a-96c9-465c-bcf2-facecea0a7d6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":613155,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":350,\"stage_id\":34}} {\"correlation_id\":\"6907203d-5426-446b-b0ed-cb3a79828876\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613155} {\"correlation_id\":\"6907203d-5426-446b-b0ed-cb3a79828876\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613155,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"6907203d-5426-446b-b0ed-cb3a79828876\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":613155,\"participants\":[{\"id\":1000742,\"user_id\":253,\"contact_id\":null,\"lead_id\":null},{\"id\":1000743,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null}]} {\"correlation_id\":\"6907203d-5426-446b-b0ed-cb3a79828876\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"6907203d-5426-446b-b0ed-cb3a79828876\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"6907203d-5426-446b-b0ed-cb3a79828876\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"6907203d-5426-446b-b0ed-cb3a79828876\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"6907203d-5426-446b-b0ed-cb3a79828876\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":613155,\"team_id\":2,\"email\":\"preslava.ivanova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"6907203d-5426-446b-b0ed-cb3a79828876\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"6907203d-5426-446b-b0ed-cb3a79828876\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":613155,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"6907203d-5426-446b-b0ed-cb3a79828876\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613155} {\"correlation_id\":\"6907203d-5426-446b-b0ed-cb3a79828876\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613155,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"6907203d-5426-446b-b0ed-cb3a79828876\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":613155,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":350,\"stage_id\":34} {\"correlation_id\":\"6907203d-5426-446b-b0ed-cb3a79828876\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":613130,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36}} {\"correlation_id\":\"b39eb37f-ecba-45cf-99d3-dc45ee004976\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613130} {\"correlation_id\":\"b39eb37f-ecba-45cf-99d3-dc45ee004976\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613130,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"b39eb37f-ecba-45cf-99d3-dc45ee004976\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":613130,\"participants\":[{\"id\":1000693,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null},{\"id\":1000694,\"user_id\":261,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"b39eb37f-ecba-45cf-99d3-dc45ee004976\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"b39eb37f-ecba-45cf-99d3-dc45ee004976\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"b39eb37f-ecba-45cf-99d3-dc45ee004976\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"b39eb37f-ecba-45cf-99d3-dc45ee004976\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"b39eb37f-ecba-45cf-99d3-dc45ee004976\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"b39eb37f-ecba-45cf-99d3-dc45ee004976\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":613130,\"team_id\":2,\"email\":\"adelina.petrova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"b39eb37f-ecba-45cf-99d3-dc45ee004976\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":613130,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"b39eb37f-ecba-45cf-99d3-dc45ee004976\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":613130} {\"correlation_id\":\"b39eb37f-ecba-45cf-99d3-dc45ee004976\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":613130,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"b39eb37f-ecba-45cf-99d3-dc45ee004976\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":613130,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36} {\"correlation_id\":\"b39eb37f-ecba-45cf-99d3-dc45ee004976\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612924,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":165,\"stage_id\":89}} {\"correlation_id\":\"22353f3a-e0aa-43eb-9a06-f618d38746e7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612924} {\"correlation_id\":\"22353f3a-e0aa-43eb-9a06-f618d38746e7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612924,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"22353f3a-e0aa-43eb-9a06-f618d38746e7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612924,\"participants\":[{\"id\":1000290,\"user_id\":19,\"contact_id\":null,\"lead_id\":null},{\"id\":1000291,\"user_id\":null,\"contact_id\":97,\"lead_id\":null}]} {\"correlation_id\":\"22353f3a-e0aa-43eb-9a06-f618d38746e7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"22353f3a-e0aa-43eb-9a06-f618d38746e7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"22353f3a-e0aa-43eb-9a06-f618d38746e7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"22353f3a-e0aa-43eb-9a06-f618d38746e7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"22353f3a-e0aa-43eb-9a06-f618d38746e7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612924,\"team_id\":2,\"email\":\"james.graham@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"22353f3a-e0aa-43eb-9a06-f618d38746e7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: ProspectCache - Searching DB for opportunity by owner {\"account_id\":69,\"contact_id\":97,\"owner_id\":19} {\"correlation_id\":\"22353f3a-e0aa-43eb-9a06-f618d38746e7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: ProspectCache - Opportunity DB search results {\"account_id\":69,\"contact_id\":97,\"opportunity_id\":165} {\"correlation_id\":\"22353f3a-e0aa-43eb-9a06-f618d38746e7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinsoncrusoe@test.com\"} {\"correlation_id\":\"22353f3a-e0aa-43eb-9a06-f618d38746e7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612924,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"22353f3a-e0aa-43eb-9a06-f618d38746e7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612924} {\"correlation_id\":\"22353f3a-e0aa-43eb-9a06-f618d38746e7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612924,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"22353f3a-e0aa-43eb-9a06-f618d38746e7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612924,\"remote_search\":true,\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":165,\"stage_id\":89} {\"correlation_id\":\"22353f3a-e0aa-43eb-9a06-f618d38746e7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612923,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":165,\"stage_id\":89}} {\"correlation_id\":\"602cc3c5-932c-4d3d-9370-35217adac6e0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612923} {\"correlation_id\":\"602cc3c5-932c-4d3d-9370-35217adac6e0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612923,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"602cc3c5-932c-4d3d-9370-35217adac6e0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612923,\"participants\":[{\"id\":1000288,\"user_id\":19,\"contact_id\":null,\"lead_id\":null},{\"id\":1000289,\"user_id\":null,\"contact_id\":97,\"lead_id\":null}]} {\"correlation_id\":\"602cc3c5-932c-4d3d-9370-35217adac6e0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"602cc3c5-932c-4d3d-9370-35217adac6e0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"602cc3c5-932c-4d3d-9370-35217adac6e0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"602cc3c5-932c-4d3d-9370-35217adac6e0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"602cc3c5-932c-4d3d-9370-35217adac6e0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612923,\"team_id\":2,\"email\":\"james.graham@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"602cc3c5-932c-4d3d-9370-35217adac6e0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinsoncrusoe@test.com\"} {\"correlation_id\":\"602cc3c5-932c-4d3d-9370-35217adac6e0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612923,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"602cc3c5-932c-4d3d-9370-35217adac6e0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612923} {\"correlation_id\":\"602cc3c5-932c-4d3d-9370-35217adac6e0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612923,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"602cc3c5-932c-4d3d-9370-35217adac6e0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612923,\"remote_search\":true,\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":165,\"stage_id\":89} {\"correlation_id\":\"602cc3c5-932c-4d3d-9370-35217adac6e0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612922,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":165,\"stage_id\":89}} {\"correlation_id\":\"4272a600-7709-4e0b-9ddd-2bf9a93ee051\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612922} {\"correlation_id\":\"4272a600-7709-4e0b-9ddd-2bf9a93ee051\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612922,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"4272a600-7709-4e0b-9ddd-2bf9a93ee051\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612922,\"participants\":[{\"id\":1000286,\"user_id\":19,\"contact_id\":null,\"lead_id\":null},{\"id\":1000287,\"user_id\":null,\"contact_id\":97,\"lead_id\":null}]} {\"correlation_id\":\"4272a600-7709-4e0b-9ddd-2bf9a93ee051\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"4272a600-7709-4e0b-9ddd-2bf9a93ee051\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"4272a600-7709-4e0b-9ddd-2bf9a93ee051\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4272a600-7709-4e0b-9ddd-2bf9a93ee051\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"4272a600-7709-4e0b-9ddd-2bf9a93ee051\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612922,\"team_id\":2,\"email\":\"james.graham@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"4272a600-7709-4e0b-9ddd-2bf9a93ee051\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinsoncrusoe@test.com\"} {\"correlation_id\":\"4272a600-7709-4e0b-9ddd-2bf9a93ee051\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612922,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"4272a600-7709-4e0b-9ddd-2bf9a93ee051\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612922} {\"correlation_id\":\"4272a600-7709-4e0b-9ddd-2bf9a93ee051\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612922,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"4272a600-7709-4e0b-9ddd-2bf9a93ee051\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612922,\"remote_search\":true,\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":165,\"stage_id\":89} {\"correlation_id\":\"4272a600-7709-4e0b-9ddd-2bf9a93ee051\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612847,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36}} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612847} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612847,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612847,\"participants\":[{\"id\":1000130,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":1000131,\"user_id\":261,\"contact_id\":null,\"lead_id\":null},{\"id\":1000151,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null}]} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [Prospect match] Cache miss, calling the API {\"identifier_type\":\"email\",\"identifier\":\"adelina.petrova@jiminny.com\"} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [Hubspot] Failed to fetch contact {\"email\":\"adelina.petrova@jiminny.com\",\"reason\":\"[404] Client error: `GET https://api.hubapi.com/crm/v3/objects/contacts/adelina.petrova%40jiminny.com?properties=email%2Cfirstname%2Clastname%2Ccountry%2Cphone%2Cmobilephone%2Cjobtitle%2Chubspot_owner_id%2Cassociatedcompanyid%2Cphoto&archived=0&idProperty=email` resulted in a `404 Not Found` response\"} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:26] local.INFO: [Prospect match] API returned empty result, caching the miss with empty prospect data {\"identifier_type\":\"email\",\"identifier\":\"adelina.petrova@jiminny.com\"} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:27] local.INFO: [Hubspot] Pagination completed {\"team_id\":2,\"endpoint\":\"https://api.hubapi.com/crm/v3/objects/contact/search\",\"total_requests\":1,\"total_records_fetched\":0,\"total_elapsed_seconds\":0.22,\"average_seconds_per_request\":0.22} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:27] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"adelina.petrova@jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:27] local.INFO: [Prospect match] Cache miss {\"identifier_type\":\"domain\",\"identifier\":\"jiminny.com\",\"crm\":\"hubspot\"} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:27] local.INFO: [Prospect match] Cache miss, calling the API {\"identifier_type\":\"domain\",\"identifier\":\"jiminny.com\"} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:27] local.INFO: [HubSpot] importAccount {\"crm_provider_id\":\"749766179\",\"config_id\":2} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:27] local.INFO: [HubSpot] CRM Search requested {\"request\":{\"filterGroups\":[{\"filters\":[{\"propertyName\":\"associations.company\",\"operator\":\"EQ\",\"value\":\"749766179\"},{\"propertyName\":\"dealstage\",\"operator\":\"NOT_IN\",\"values\":[\"closedwon\",\"4040964\",\"59247967\"]},{\"propertyName\":\"dealstage\",\"operator\":\"NOT_IN\",\"values\":[\"closedlost\",\"4040965\",\"59247968\"]}]}],\"sorts\":[{\"propertyName\":\"modifieddate\",\"direction\":\"DESCENDING\"}],\"properties\":[\"dealname\",\"amount\",\"hubspot_owner_id\",\"pipeline\",\"dealstage\",\"closedate\",\"deal_currency_code\"],\"limit\":200}} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:27] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"6bdeb8f0-c361-4cb3-9fb1-c6bdf466a73e\",\"trace_id\":\"d050352e-9bf5-497a-99d6-e46661a04a04\"}\n[2026-05-11 10:43:27] local.INFO: [Hubspot] Pagination completed {\"team_id\":2,\"endpoint\":\"https://api.hubapi.com/crm/v3/objects/deals/search\",\"total_requests\":1,\"total_records_fetched\":10,\"total_elapsed_seconds\":0.25,\"average_seconds_per_request\":0.25} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:27] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:skip-lists:refresh\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":60.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"6bdeb8f0-c361-4cb3-9fb1-c6bdf466a73e\",\"trace_id\":\"d050352e-9bf5-497a-99d6-e46661a04a04\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612847,\"team_id\":2,\"email\":\"adelina.petrova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612847,\"participants_processed\":3,\"exact_matches\":1,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612847} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612847,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612847,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36} {\"correlation_id\":\"e32e0cef-947c-4003-bbde-7e8732b544f7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612822,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36}} {\"correlation_id\":\"a8970854-ad6a-4751-9cc5-ffe651d20f06\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612822} {\"correlation_id\":\"a8970854-ad6a-4751-9cc5-ffe651d20f06\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612822,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"a8970854-ad6a-4751-9cc5-ffe651d20f06\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612822,\"participants\":[{\"id\":1000080,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null},{\"id\":1000081,\"user_id\":261,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"a8970854-ad6a-4751-9cc5-ffe651d20f06\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"a8970854-ad6a-4751-9cc5-ffe651d20f06\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"a8970854-ad6a-4751-9cc5-ffe651d20f06\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"a8970854-ad6a-4751-9cc5-ffe651d20f06\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"a8970854-ad6a-4751-9cc5-ffe651d20f06\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"a8970854-ad6a-4751-9cc5-ffe651d20f06\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612822,\"team_id\":2,\"email\":\"adelina.petrova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"a8970854-ad6a-4751-9cc5-ffe651d20f06\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612822,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"a8970854-ad6a-4751-9cc5-ffe651d20f06\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612822} {\"correlation_id\":\"a8970854-ad6a-4751-9cc5-ffe651d20f06\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612822,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"a8970854-ad6a-4751-9cc5-ffe651d20f06\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612822,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36} {\"correlation_id\":\"a8970854-ad6a-4751-9cc5-ffe651d20f06\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612819,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36}} {\"correlation_id\":\"16e6bc16-5ab8-49ce-b7da-c292b8c94105\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612819} {\"correlation_id\":\"16e6bc16-5ab8-49ce-b7da-c292b8c94105\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612819,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"16e6bc16-5ab8-49ce-b7da-c292b8c94105\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612819,\"participants\":[{\"id\":1000073,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null},{\"id\":1000074,\"user_id\":261,\"contact_id\":null,\"lead_id\":null},{\"id\":1000075,\"user_id\":null,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"16e6bc16-5ab8-49ce-b7da-c292b8c94105\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"16e6bc16-5ab8-49ce-b7da-c292b8c94105\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"16e6bc16-5ab8-49ce-b7da-c292b8c94105\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"16e6bc16-5ab8-49ce-b7da-c292b8c94105\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"16e6bc16-5ab8-49ce-b7da-c292b8c94105\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"16e6bc16-5ab8-49ce-b7da-c292b8c94105\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612819,\"team_id\":2,\"email\":\"adelina.petrova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"16e6bc16-5ab8-49ce-b7da-c292b8c94105\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"adelina.petrova@jiminny.com\"} {\"correlation_id\":\"16e6bc16-5ab8-49ce-b7da-c292b8c94105\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"adelina.petrova@jiminny.com\"} {\"correlation_id\":\"16e6bc16-5ab8-49ce-b7da-c292b8c94105\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"adelina.petrova@jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"16e6bc16-5ab8-49ce-b7da-c292b8c94105\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612819,\"participants_processed\":3,\"exact_matches\":1,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"16e6bc16-5ab8-49ce-b7da-c292b8c94105\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612819} {\"correlation_id\":\"16e6bc16-5ab8-49ce-b7da-c292b8c94105\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612819,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"16e6bc16-5ab8-49ce-b7da-c292b8c94105\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612819,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36} {\"correlation_id\":\"16e6bc16-5ab8-49ce-b7da-c292b8c94105\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612673,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36}} {\"correlation_id\":\"8711a017-62dd-479f-b84e-123d6e3f40be\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612673} {\"correlation_id\":\"8711a017-62dd-479f-b84e-123d6e3f40be\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612673,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"8711a017-62dd-479f-b84e-123d6e3f40be\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612673,\"participants\":[{\"id\":999993,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null},{\"id\":999994,\"user_id\":261,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"8711a017-62dd-479f-b84e-123d6e3f40be\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"8711a017-62dd-479f-b84e-123d6e3f40be\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"8711a017-62dd-479f-b84e-123d6e3f40be\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"8711a017-62dd-479f-b84e-123d6e3f40be\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"8711a017-62dd-479f-b84e-123d6e3f40be\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"8711a017-62dd-479f-b84e-123d6e3f40be\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612673,\"team_id\":2,\"email\":\"adelina.petrova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"8711a017-62dd-479f-b84e-123d6e3f40be\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612673,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"8711a017-62dd-479f-b84e-123d6e3f40be\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612673} {\"correlation_id\":\"8711a017-62dd-479f-b84e-123d6e3f40be\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612673,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"8711a017-62dd-479f-b84e-123d6e3f40be\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612673,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36} {\"correlation_id\":\"8711a017-62dd-479f-b84e-123d6e3f40be\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612642,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36}} {\"correlation_id\":\"aa7b8829-1739-4f9b-aad1-c6868d8679ca\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612642} {\"correlation_id\":\"aa7b8829-1739-4f9b-aad1-c6868d8679ca\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612642,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"aa7b8829-1739-4f9b-aad1-c6868d8679ca\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612642,\"participants\":[{\"id\":999935,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null},{\"id\":999936,\"user_id\":261,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"aa7b8829-1739-4f9b-aad1-c6868d8679ca\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"aa7b8829-1739-4f9b-aad1-c6868d8679ca\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"aa7b8829-1739-4f9b-aad1-c6868d8679ca\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"aa7b8829-1739-4f9b-aad1-c6868d8679ca\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"aa7b8829-1739-4f9b-aad1-c6868d8679ca\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"aa7b8829-1739-4f9b-aad1-c6868d8679ca\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612642,\"team_id\":2,\"email\":\"adelina.petrova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"aa7b8829-1739-4f9b-aad1-c6868d8679ca\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612642,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"aa7b8829-1739-4f9b-aad1-c6868d8679ca\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612642} {\"correlation_id\":\"aa7b8829-1739-4f9b-aad1-c6868d8679ca\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612642,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"aa7b8829-1739-4f9b-aad1-c6868d8679ca\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612642,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36} {\"correlation_id\":\"aa7b8829-1739-4f9b-aad1-c6868d8679ca\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612598,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"2fef3119-c9e0-421d-b77e-3cf1e26b480a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612598} {\"correlation_id\":\"2fef3119-c9e0-421d-b77e-3cf1e26b480a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612598,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"2fef3119-c9e0-421d-b77e-3cf1e26b480a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612598,\"participants\":[{\"id\":999857,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null},{\"id\":999858,\"user_id\":206,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"2fef3119-c9e0-421d-b77e-3cf1e26b480a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"2fef3119-c9e0-421d-b77e-3cf1e26b480a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"2fef3119-c9e0-421d-b77e-3cf1e26b480a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"2fef3119-c9e0-421d-b77e-3cf1e26b480a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"2fef3119-c9e0-421d-b77e-3cf1e26b480a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: ProspectCache - Searching DB for opportunity by owner {\"account_id\":243,\"contact_id\":4491,\"owner_id\":206} {\"correlation_id\":\"2fef3119-c9e0-421d-b77e-3cf1e26b480a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: ProspectCache - Fallback DB opportunity search {\"account_id\":243,\"contact_id\":4491} {\"correlation_id\":\"2fef3119-c9e0-421d-b77e-3cf1e26b480a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: ProspectCache - Opportunity DB search results {\"account_id\":243,\"contact_id\":4491,\"opportunity_id\":276} {\"correlation_id\":\"2fef3119-c9e0-421d-b77e-3cf1e26b480a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"2fef3119-c9e0-421d-b77e-3cf1e26b480a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612598,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"2fef3119-c9e0-421d-b77e-3cf1e26b480a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612598,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"2fef3119-c9e0-421d-b77e-3cf1e26b480a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612598} {\"correlation_id\":\"2fef3119-c9e0-421d-b77e-3cf1e26b480a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612598,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"2fef3119-c9e0-421d-b77e-3cf1e26b480a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612598,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"2fef3119-c9e0-421d-b77e-3cf1e26b480a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612597,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"f4aa8c68-1048-465c-8ab6-e414220b2b3d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612597} {\"correlation_id\":\"f4aa8c68-1048-465c-8ab6-e414220b2b3d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612597,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"f4aa8c68-1048-465c-8ab6-e414220b2b3d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612597,\"participants\":[{\"id\":999855,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999856,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null}]} {\"correlation_id\":\"f4aa8c68-1048-465c-8ab6-e414220b2b3d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"f4aa8c68-1048-465c-8ab6-e414220b2b3d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"f4aa8c68-1048-465c-8ab6-e414220b2b3d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"f4aa8c68-1048-465c-8ab6-e414220b2b3d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"f4aa8c68-1048-465c-8ab6-e414220b2b3d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612597,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"f4aa8c68-1048-465c-8ab6-e414220b2b3d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"f4aa8c68-1048-465c-8ab6-e414220b2b3d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612597,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"f4aa8c68-1048-465c-8ab6-e414220b2b3d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612597} {\"correlation_id\":\"f4aa8c68-1048-465c-8ab6-e414220b2b3d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612597,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"f4aa8c68-1048-465c-8ab6-e414220b2b3d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612597,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"f4aa8c68-1048-465c-8ab6-e414220b2b3d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612596,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"885d60db-2622-4f69-9826-b15464181dcb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612596} {\"correlation_id\":\"885d60db-2622-4f69-9826-b15464181dcb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612596,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"885d60db-2622-4f69-9826-b15464181dcb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612596,\"participants\":[{\"id\":999853,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999854,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null}]} {\"correlation_id\":\"885d60db-2622-4f69-9826-b15464181dcb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"885d60db-2622-4f69-9826-b15464181dcb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"885d60db-2622-4f69-9826-b15464181dcb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"885d60db-2622-4f69-9826-b15464181dcb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"885d60db-2622-4f69-9826-b15464181dcb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612596,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"885d60db-2622-4f69-9826-b15464181dcb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"885d60db-2622-4f69-9826-b15464181dcb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612596,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"885d60db-2622-4f69-9826-b15464181dcb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612596} {\"correlation_id\":\"885d60db-2622-4f69-9826-b15464181dcb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612596,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"885d60db-2622-4f69-9826-b15464181dcb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612596,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"885d60db-2622-4f69-9826-b15464181dcb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612595,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"0bc8a5e9-778b-4d99-bf0c-d903bd129c55\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612595} {\"correlation_id\":\"0bc8a5e9-778b-4d99-bf0c-d903bd129c55\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612595,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"0bc8a5e9-778b-4d99-bf0c-d903bd129c55\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612595,\"participants\":[{\"id\":999851,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null},{\"id\":999852,\"user_id\":206,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"0bc8a5e9-778b-4d99-bf0c-d903bd129c55\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"0bc8a5e9-778b-4d99-bf0c-d903bd129c55\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"0bc8a5e9-778b-4d99-bf0c-d903bd129c55\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"0bc8a5e9-778b-4d99-bf0c-d903bd129c55\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"0bc8a5e9-778b-4d99-bf0c-d903bd129c55\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"0bc8a5e9-778b-4d99-bf0c-d903bd129c55\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612595,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"0bc8a5e9-778b-4d99-bf0c-d903bd129c55\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612595,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"0bc8a5e9-778b-4d99-bf0c-d903bd129c55\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612595} {\"correlation_id\":\"0bc8a5e9-778b-4d99-bf0c-d903bd129c55\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612595,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"0bc8a5e9-778b-4d99-bf0c-d903bd129c55\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612595,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"0bc8a5e9-778b-4d99-bf0c-d903bd129c55\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612594,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"9710b4b5-e8f1-4fbf-b865-162ea340046c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612594} {\"correlation_id\":\"9710b4b5-e8f1-4fbf-b865-162ea340046c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612594,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"9710b4b5-e8f1-4fbf-b865-162ea340046c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612594,\"participants\":[{\"id\":999849,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999850,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null}]} {\"correlation_id\":\"9710b4b5-e8f1-4fbf-b865-162ea340046c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"9710b4b5-e8f1-4fbf-b865-162ea340046c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"9710b4b5-e8f1-4fbf-b865-162ea340046c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"9710b4b5-e8f1-4fbf-b865-162ea340046c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"9710b4b5-e8f1-4fbf-b865-162ea340046c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612594,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"9710b4b5-e8f1-4fbf-b865-162ea340046c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"9710b4b5-e8f1-4fbf-b865-162ea340046c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612594,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"9710b4b5-e8f1-4fbf-b865-162ea340046c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612594} {\"correlation_id\":\"9710b4b5-e8f1-4fbf-b865-162ea340046c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612594,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"9710b4b5-e8f1-4fbf-b865-162ea340046c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612594,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"9710b4b5-e8f1-4fbf-b865-162ea340046c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612593,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"59a8137b-0518-4c91-b63d-c794df57a8c1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612593} {\"correlation_id\":\"59a8137b-0518-4c91-b63d-c794df57a8c1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612593,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"59a8137b-0518-4c91-b63d-c794df57a8c1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612593,\"participants\":[{\"id\":999847,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999848,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null}]} {\"correlation_id\":\"59a8137b-0518-4c91-b63d-c794df57a8c1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"59a8137b-0518-4c91-b63d-c794df57a8c1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"59a8137b-0518-4c91-b63d-c794df57a8c1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"59a8137b-0518-4c91-b63d-c794df57a8c1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"59a8137b-0518-4c91-b63d-c794df57a8c1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612593,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"59a8137b-0518-4c91-b63d-c794df57a8c1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"59a8137b-0518-4c91-b63d-c794df57a8c1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612593,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"59a8137b-0518-4c91-b63d-c794df57a8c1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612593} {\"correlation_id\":\"59a8137b-0518-4c91-b63d-c794df57a8c1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612593,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"59a8137b-0518-4c91-b63d-c794df57a8c1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612593,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"59a8137b-0518-4c91-b63d-c794df57a8c1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612592,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"e1f2c9b0-a985-4277-b0da-5190fb9b25d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612592} {\"correlation_id\":\"e1f2c9b0-a985-4277-b0da-5190fb9b25d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612592,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"e1f2c9b0-a985-4277-b0da-5190fb9b25d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612592,\"participants\":[{\"id\":999845,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null},{\"id\":999846,\"user_id\":206,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"e1f2c9b0-a985-4277-b0da-5190fb9b25d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"e1f2c9b0-a985-4277-b0da-5190fb9b25d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"e1f2c9b0-a985-4277-b0da-5190fb9b25d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"e1f2c9b0-a985-4277-b0da-5190fb9b25d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"e1f2c9b0-a985-4277-b0da-5190fb9b25d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"e1f2c9b0-a985-4277-b0da-5190fb9b25d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612592,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"e1f2c9b0-a985-4277-b0da-5190fb9b25d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612592,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"e1f2c9b0-a985-4277-b0da-5190fb9b25d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612592} {\"correlation_id\":\"e1f2c9b0-a985-4277-b0da-5190fb9b25d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612592,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"e1f2c9b0-a985-4277-b0da-5190fb9b25d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612592,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"e1f2c9b0-a985-4277-b0da-5190fb9b25d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612591,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"8a5f33ea-490a-41e3-b71a-872c52626583\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612591} {\"correlation_id\":\"8a5f33ea-490a-41e3-b71a-872c52626583\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612591,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"8a5f33ea-490a-41e3-b71a-872c52626583\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612591,\"participants\":[{\"id\":999843,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999844,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null}]} {\"correlation_id\":\"8a5f33ea-490a-41e3-b71a-872c52626583\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"8a5f33ea-490a-41e3-b71a-872c52626583\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"8a5f33ea-490a-41e3-b71a-872c52626583\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"8a5f33ea-490a-41e3-b71a-872c52626583\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"8a5f33ea-490a-41e3-b71a-872c52626583\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612591,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"8a5f33ea-490a-41e3-b71a-872c52626583\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"8a5f33ea-490a-41e3-b71a-872c52626583\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612591,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"8a5f33ea-490a-41e3-b71a-872c52626583\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612591} {\"correlation_id\":\"8a5f33ea-490a-41e3-b71a-872c52626583\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612591,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"8a5f33ea-490a-41e3-b71a-872c52626583\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612591,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"8a5f33ea-490a-41e3-b71a-872c52626583\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:28] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612590,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"b34fb574-2229-4ff9-b572-7e2f801295a8\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612590} {\"correlation_id\":\"b34fb574-2229-4ff9-b572-7e2f801295a8\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612590,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"b34fb574-2229-4ff9-b572-7e2f801295a8\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612590,\"participants\":[{\"id\":999841,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999842,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null}]} {\"correlation_id\":\"b34fb574-2229-4ff9-b572-7e2f801295a8\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"b34fb574-2229-4ff9-b572-7e2f801295a8\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"b34fb574-2229-4ff9-b572-7e2f801295a8\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"b34fb574-2229-4ff9-b572-7e2f801295a8\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"b34fb574-2229-4ff9-b572-7e2f801295a8\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612590,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"b34fb574-2229-4ff9-b572-7e2f801295a8\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"b34fb574-2229-4ff9-b572-7e2f801295a8\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612590,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"b34fb574-2229-4ff9-b572-7e2f801295a8\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612590} {\"correlation_id\":\"b34fb574-2229-4ff9-b572-7e2f801295a8\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612590,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"b34fb574-2229-4ff9-b572-7e2f801295a8\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612590,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"b34fb574-2229-4ff9-b572-7e2f801295a8\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612589,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"746b5162-98d0-4e35-96ca-f6756e9b2d39\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612589} {\"correlation_id\":\"746b5162-98d0-4e35-96ca-f6756e9b2d39\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612589,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"746b5162-98d0-4e35-96ca-f6756e9b2d39\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612589,\"participants\":[{\"id\":999839,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999840,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null}]} {\"correlation_id\":\"746b5162-98d0-4e35-96ca-f6756e9b2d39\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"746b5162-98d0-4e35-96ca-f6756e9b2d39\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"746b5162-98d0-4e35-96ca-f6756e9b2d39\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"746b5162-98d0-4e35-96ca-f6756e9b2d39\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"746b5162-98d0-4e35-96ca-f6756e9b2d39\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612589,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"746b5162-98d0-4e35-96ca-f6756e9b2d39\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"746b5162-98d0-4e35-96ca-f6756e9b2d39\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612589,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"746b5162-98d0-4e35-96ca-f6756e9b2d39\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612589} {\"correlation_id\":\"746b5162-98d0-4e35-96ca-f6756e9b2d39\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612589,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"746b5162-98d0-4e35-96ca-f6756e9b2d39\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612589,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"746b5162-98d0-4e35-96ca-f6756e9b2d39\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612588,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"601702ec-6d8b-4aa3-b7e2-780e758b68d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612588} {\"correlation_id\":\"601702ec-6d8b-4aa3-b7e2-780e758b68d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612588,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"601702ec-6d8b-4aa3-b7e2-780e758b68d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612588,\"participants\":[{\"id\":999837,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null},{\"id\":999838,\"user_id\":206,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"601702ec-6d8b-4aa3-b7e2-780e758b68d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"601702ec-6d8b-4aa3-b7e2-780e758b68d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"601702ec-6d8b-4aa3-b7e2-780e758b68d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"601702ec-6d8b-4aa3-b7e2-780e758b68d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"601702ec-6d8b-4aa3-b7e2-780e758b68d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"601702ec-6d8b-4aa3-b7e2-780e758b68d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612588,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"601702ec-6d8b-4aa3-b7e2-780e758b68d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612588,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"601702ec-6d8b-4aa3-b7e2-780e758b68d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612588} {\"correlation_id\":\"601702ec-6d8b-4aa3-b7e2-780e758b68d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612588,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"601702ec-6d8b-4aa3-b7e2-780e758b68d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612588,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"601702ec-6d8b-4aa3-b7e2-780e758b68d1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612587,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"75f31c21-1a08-4a7a-beef-a6ee82905eb2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612587} {\"correlation_id\":\"75f31c21-1a08-4a7a-beef-a6ee82905eb2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612587,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"75f31c21-1a08-4a7a-beef-a6ee82905eb2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612587,\"participants\":[{\"id\":999835,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999836,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null}]} {\"correlation_id\":\"75f31c21-1a08-4a7a-beef-a6ee82905eb2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"75f31c21-1a08-4a7a-beef-a6ee82905eb2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"75f31c21-1a08-4a7a-beef-a6ee82905eb2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"75f31c21-1a08-4a7a-beef-a6ee82905eb2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"75f31c21-1a08-4a7a-beef-a6ee82905eb2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612587,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"75f31c21-1a08-4a7a-beef-a6ee82905eb2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"75f31c21-1a08-4a7a-beef-a6ee82905eb2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612587,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"75f31c21-1a08-4a7a-beef-a6ee82905eb2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612587} {\"correlation_id\":\"75f31c21-1a08-4a7a-beef-a6ee82905eb2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612587,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"75f31c21-1a08-4a7a-beef-a6ee82905eb2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612587,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"75f31c21-1a08-4a7a-beef-a6ee82905eb2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612586,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"b79deb62-f26f-4831-8192-2fca40cc0445\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612586} {\"correlation_id\":\"b79deb62-f26f-4831-8192-2fca40cc0445\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612586,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"b79deb62-f26f-4831-8192-2fca40cc0445\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612586,\"participants\":[{\"id\":999833,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null},{\"id\":999834,\"user_id\":206,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"b79deb62-f26f-4831-8192-2fca40cc0445\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"b79deb62-f26f-4831-8192-2fca40cc0445\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"b79deb62-f26f-4831-8192-2fca40cc0445\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"b79deb62-f26f-4831-8192-2fca40cc0445\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"b79deb62-f26f-4831-8192-2fca40cc0445\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"b79deb62-f26f-4831-8192-2fca40cc0445\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612586,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"b79deb62-f26f-4831-8192-2fca40cc0445\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612586,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"b79deb62-f26f-4831-8192-2fca40cc0445\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612586} {\"correlation_id\":\"b79deb62-f26f-4831-8192-2fca40cc0445\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612586,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"b79deb62-f26f-4831-8192-2fca40cc0445\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612586,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"b79deb62-f26f-4831-8192-2fca40cc0445\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612585,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"2aa37843-11f3-4f11-a9ef-3277ddd89ec3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612585} {\"correlation_id\":\"2aa37843-11f3-4f11-a9ef-3277ddd89ec3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612585,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"2aa37843-11f3-4f11-a9ef-3277ddd89ec3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612585,\"participants\":[{\"id\":999831,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999832,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null}]} {\"correlation_id\":\"2aa37843-11f3-4f11-a9ef-3277ddd89ec3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"2aa37843-11f3-4f11-a9ef-3277ddd89ec3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"2aa37843-11f3-4f11-a9ef-3277ddd89ec3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"2aa37843-11f3-4f11-a9ef-3277ddd89ec3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"2aa37843-11f3-4f11-a9ef-3277ddd89ec3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612585,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"2aa37843-11f3-4f11-a9ef-3277ddd89ec3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"2aa37843-11f3-4f11-a9ef-3277ddd89ec3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612585,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"2aa37843-11f3-4f11-a9ef-3277ddd89ec3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612585} {\"correlation_id\":\"2aa37843-11f3-4f11-a9ef-3277ddd89ec3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612585,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"2aa37843-11f3-4f11-a9ef-3277ddd89ec3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612585,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"2aa37843-11f3-4f11-a9ef-3277ddd89ec3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612584,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"07bfbf03-ba7c-47ad-8252-88913ec31644\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612584} {\"correlation_id\":\"07bfbf03-ba7c-47ad-8252-88913ec31644\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612584,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"07bfbf03-ba7c-47ad-8252-88913ec31644\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612584,\"participants\":[{\"id\":999829,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999830,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null}]} {\"correlation_id\":\"07bfbf03-ba7c-47ad-8252-88913ec31644\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"07bfbf03-ba7c-47ad-8252-88913ec31644\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"07bfbf03-ba7c-47ad-8252-88913ec31644\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"07bfbf03-ba7c-47ad-8252-88913ec31644\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"07bfbf03-ba7c-47ad-8252-88913ec31644\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612584,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"07bfbf03-ba7c-47ad-8252-88913ec31644\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"07bfbf03-ba7c-47ad-8252-88913ec31644\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612584,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"07bfbf03-ba7c-47ad-8252-88913ec31644\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612584} {\"correlation_id\":\"07bfbf03-ba7c-47ad-8252-88913ec31644\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612584,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"07bfbf03-ba7c-47ad-8252-88913ec31644\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612584,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"07bfbf03-ba7c-47ad-8252-88913ec31644\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612583,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"64e9fc7f-c17e-4af3-b884-54a2c388e8f0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612583} {\"correlation_id\":\"64e9fc7f-c17e-4af3-b884-54a2c388e8f0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612583,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"64e9fc7f-c17e-4af3-b884-54a2c388e8f0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612583,\"participants\":[{\"id\":999827,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null},{\"id\":999828,\"user_id\":206,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"64e9fc7f-c17e-4af3-b884-54a2c388e8f0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"64e9fc7f-c17e-4af3-b884-54a2c388e8f0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"64e9fc7f-c17e-4af3-b884-54a2c388e8f0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"64e9fc7f-c17e-4af3-b884-54a2c388e8f0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"64e9fc7f-c17e-4af3-b884-54a2c388e8f0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"64e9fc7f-c17e-4af3-b884-54a2c388e8f0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612583,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"64e9fc7f-c17e-4af3-b884-54a2c388e8f0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612583,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"64e9fc7f-c17e-4af3-b884-54a2c388e8f0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612583} {\"correlation_id\":\"64e9fc7f-c17e-4af3-b884-54a2c388e8f0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612583,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"64e9fc7f-c17e-4af3-b884-54a2c388e8f0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612583,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"64e9fc7f-c17e-4af3-b884-54a2c388e8f0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612582,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"23188089-fcae-491d-b35d-c20491564cfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612582} {\"correlation_id\":\"23188089-fcae-491d-b35d-c20491564cfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612582,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"23188089-fcae-491d-b35d-c20491564cfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612582,\"participants\":[{\"id\":999825,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999826,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null}]} {\"correlation_id\":\"23188089-fcae-491d-b35d-c20491564cfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"23188089-fcae-491d-b35d-c20491564cfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"23188089-fcae-491d-b35d-c20491564cfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"23188089-fcae-491d-b35d-c20491564cfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"23188089-fcae-491d-b35d-c20491564cfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612582,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"23188089-fcae-491d-b35d-c20491564cfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"23188089-fcae-491d-b35d-c20491564cfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612582,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"23188089-fcae-491d-b35d-c20491564cfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612582} {\"correlation_id\":\"23188089-fcae-491d-b35d-c20491564cfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612582,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"23188089-fcae-491d-b35d-c20491564cfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612582,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"23188089-fcae-491d-b35d-c20491564cfe\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"9f9cc209-aee5-4d58-b4ad-1455693e83d6\",\"trace_id\":\"f11e2691-d834-4325-91f7-003e08888b34\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612581,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"91305be5-0942-40ad-86c8-c2b2f018ea2b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612581} {\"correlation_id\":\"91305be5-0942-40ad-86c8-c2b2f018ea2b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612581,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"91305be5-0942-40ad-86c8-c2b2f018ea2b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612581,\"participants\":[{\"id\":999823,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null},{\"id\":999824,\"user_id\":206,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"91305be5-0942-40ad-86c8-c2b2f018ea2b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"91305be5-0942-40ad-86c8-c2b2f018ea2b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"91305be5-0942-40ad-86c8-c2b2f018ea2b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"91305be5-0942-40ad-86c8-c2b2f018ea2b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"91305be5-0942-40ad-86c8-c2b2f018ea2b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"91305be5-0942-40ad-86c8-c2b2f018ea2b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612581,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"91305be5-0942-40ad-86c8-c2b2f018ea2b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612581,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"91305be5-0942-40ad-86c8-c2b2f018ea2b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612581} {\"correlation_id\":\"91305be5-0942-40ad-86c8-c2b2f018ea2b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612581,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"91305be5-0942-40ad-86c8-c2b2f018ea2b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [EmailSchedule] STARTING batch process {\"host\":\"docker_lamp_1\"} {\"correlation_id\":\"9f9cc209-aee5-4d58-b4ad-1455693e83d6\",\"trace_id\":\"f11e2691-d834-4325-91f7-003e08888b34\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612581,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"91305be5-0942-40ad-86c8-c2b2f018ea2b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612565,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"be6ed1a4-4495-4ec9-932f-da6698c31cec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [EmailSchedule] FINISHED batch process {\"host\":\"docker_lamp_1\",\"processed\":0} {\"correlation_id\":\"9f9cc209-aee5-4d58-b4ad-1455693e83d6\",\"trace_id\":\"f11e2691-d834-4325-91f7-003e08888b34\"}\n[2026-05-11 10:43:29] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:process\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":60.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"9f9cc209-aee5-4d58-b4ad-1455693e83d6\",\"trace_id\":\"f11e2691-d834-4325-91f7-003e08888b34\"}\n[2026-05-11 10:43:29] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612565} {\"correlation_id\":\"be6ed1a4-4495-4ec9-932f-da6698c31cec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612565,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"be6ed1a4-4495-4ec9-932f-da6698c31cec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612565,\"participants\":[{\"id\":999789,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999790,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null}]} {\"correlation_id\":\"be6ed1a4-4495-4ec9-932f-da6698c31cec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"be6ed1a4-4495-4ec9-932f-da6698c31cec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"be6ed1a4-4495-4ec9-932f-da6698c31cec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"be6ed1a4-4495-4ec9-932f-da6698c31cec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"be6ed1a4-4495-4ec9-932f-da6698c31cec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612565,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"be6ed1a4-4495-4ec9-932f-da6698c31cec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"be6ed1a4-4495-4ec9-932f-da6698c31cec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612565,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"be6ed1a4-4495-4ec9-932f-da6698c31cec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612565} {\"correlation_id\":\"be6ed1a4-4495-4ec9-932f-da6698c31cec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612565,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"be6ed1a4-4495-4ec9-932f-da6698c31cec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612565,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"be6ed1a4-4495-4ec9-932f-da6698c31cec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612563,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":350,\"stage_id\":34}} {\"correlation_id\":\"77b39c9c-1e21-4c1d-925b-4acce1b204d3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612563} {\"correlation_id\":\"77b39c9c-1e21-4c1d-925b-4acce1b204d3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612563,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"77b39c9c-1e21-4c1d-925b-4acce1b204d3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612563,\"participants\":[{\"id\":999784,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999785,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null}]} {\"correlation_id\":\"77b39c9c-1e21-4c1d-925b-4acce1b204d3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"77b39c9c-1e21-4c1d-925b-4acce1b204d3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"77b39c9c-1e21-4c1d-925b-4acce1b204d3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"77b39c9c-1e21-4c1d-925b-4acce1b204d3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"77b39c9c-1e21-4c1d-925b-4acce1b204d3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612563,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"77b39c9c-1e21-4c1d-925b-4acce1b204d3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: ProspectCache - Searching DB for opportunity by owner {\"account_id\":244,\"contact_id\":4487,\"owner_id\":206} {\"correlation_id\":\"77b39c9c-1e21-4c1d-925b-4acce1b204d3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: ProspectCache - Fallback DB opportunity search {\"account_id\":244,\"contact_id\":4487} {\"correlation_id\":\"77b39c9c-1e21-4c1d-925b-4acce1b204d3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: ProspectCache - Opportunity DB search results {\"account_id\":244,\"contact_id\":4487,\"opportunity_id\":350} {\"correlation_id\":\"77b39c9c-1e21-4c1d-925b-4acce1b204d3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:29] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"77b39c9c-1e21-4c1d-925b-4acce1b204d3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:30] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612563,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"77b39c9c-1e21-4c1d-925b-4acce1b204d3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:30] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612563} {\"correlation_id\":\"77b39c9c-1e21-4c1d-925b-4acce1b204d3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:30] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612563,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"77b39c9c-1e21-4c1d-925b-4acce1b204d3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:30] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612563,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":350,\"stage_id\":34} {\"correlation_id\":\"77b39c9c-1e21-4c1d-925b-4acce1b204d3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:30] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612562,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"485e0be1-a971-4cde-9ead-4276768bdf66\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:30] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612562} {\"correlation_id\":\"485e0be1-a971-4cde-9ead-4276768bdf66\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:30] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612562,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"485e0be1-a971-4cde-9ead-4276768bdf66\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:30] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612562,\"participants\":[{\"id\":999782,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":999783,\"user_id\":206,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"485e0be1-a971-4cde-9ead-4276768bdf66\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:30] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"485e0be1-a971-4cde-9ead-4276768bdf66\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:30] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"485e0be1-a971-4cde-9ead-4276768bdf66\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:30] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"485e0be1-a971-4cde-9ead-4276768bdf66\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:30] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"485e0be1-a971-4cde-9ead-4276768bdf66\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:30] local.INFO: [Prospect match] Cache miss, calling the API {\"identifier_type\":\"email\",\"identifier\":\"447782589921@txt.staging.jiminny.com\"} {\"correlation_id\":\"485e0be1-a971-4cde-9ead-4276768bdf66\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:30] local.INFO: [Hubspot] Failed to fetch contact {\"email\":\"447782589921@txt.staging.jiminny.com\",\"reason\":\"[404] Client error: `GET https://api.hubapi.com/crm/v3/objects/contacts/447782589921%40txt.staging.jiminny.com?properties=email%2Cfirstname%2Clastname%2Ccountry%2Cphone%2Cmobilephone%2Cjobtitle%2Chubspot_owner_id%2Cassociatedcompanyid%2Cphoto&archived=0&idProperty=email` resulted in a `404 Not Found` response\"} {\"correlation_id\":\"485e0be1-a971-4cde-9ead-4276768bdf66\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:30] local.INFO: [Prospect match] API returned empty result, caching the miss with empty prospect data {\"identifier_type\":\"email\",\"identifier\":\"447782589921@txt.staging.jiminny.com\"} {\"correlation_id\":\"485e0be1-a971-4cde-9ead-4276768bdf66\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:30] local.INFO: [Hubspot] Pagination completed {\"team_id\":2,\"endpoint\":\"https://api.hubapi.com/crm/v3/objects/contact/search\",\"total_requests\":1,\"total_records_fetched\":0,\"total_elapsed_seconds\":0.22,\"average_seconds_per_request\":0.22} {\"correlation_id\":\"485e0be1-a971-4cde-9ead-4276768bdf66\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:30] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"447782589921@txt.staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"485e0be1-a971-4cde-9ead-4276768bdf66\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:30] local.INFO: [Prospect match] Cache miss {\"identifier_type\":\"domain\",\"identifier\":\"jiminny.com\",\"crm\":\"hubspot\"} {\"correlation_id\":\"485e0be1-a971-4cde-9ead-4276768bdf66\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:30] local.INFO: [Prospect match] Cache miss, calling the API {\"identifier_type\":\"domain\",\"identifier\":\"jiminny.com\"} {\"correlation_id\":\"485e0be1-a971-4cde-9ead-4276768bdf66\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:31] local.INFO: [HubSpot] importAccount {\"crm_provider_id\":\"749766179\",\"config_id\":2} {\"correlation_id\":\"485e0be1-a971-4cde-9ead-4276768bdf66\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:31] local.INFO: [HubSpot] CRM Search requested {\"request\":{\"filterGroups\":[{\"filters\":[{\"propertyName\":\"associations.company\",\"operator\":\"EQ\",\"value\":\"749766179\"},{\"propertyName\":\"dealstage\",\"operator\":\"NOT_IN\",\"values\":[\"closedwon\",\"4040964\",\"59247967\"]},{\"propertyName\":\"dealstage\",\"operator\":\"NOT_IN\",\"values\":[\"closedlost\",\"4040965\",\"59247968\"]}]}],\"sorts\":[{\"propertyName\":\"modifieddate\",\"direction\":\"DESCENDING\"}],\"properties\":[\"dealname\",\"amount\",\"hubspot_owner_id\",\"pipeline\",\"dealstage\",\"closedate\",\"deal_currency_code\"],\"limit\":200}} {\"correlation_id\":\"485e0be1-a971-4cde-9ead-4276768bdf66\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:31] local.INFO: [Hubspot] Pagination completed {\"team_id\":2,\"endpoint\":\"https://api.hubapi.com/crm/v3/objects/deals/search\",\"total_requests\":1,\"total_records_fetched\":10,\"total_elapsed_seconds\":0.3,\"average_seconds_per_request\":0.3} {\"correlation_id\":\"485e0be1-a971-4cde-9ead-4276768bdf66\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612562,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"485e0be1-a971-4cde-9ead-4276768bdf66\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612562,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"485e0be1-a971-4cde-9ead-4276768bdf66\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612562} {\"correlation_id\":\"485e0be1-a971-4cde-9ead-4276768bdf66\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612562,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"485e0be1-a971-4cde-9ead-4276768bdf66\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612562,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"485e0be1-a971-4cde-9ead-4276768bdf66\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612561,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"9fffce69-30ed-4c66-9ee9-e980e776cd57\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612561} {\"correlation_id\":\"9fffce69-30ed-4c66-9ee9-e980e776cd57\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612561,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"9fffce69-30ed-4c66-9ee9-e980e776cd57\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612561,\"participants\":[{\"id\":999780,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999781,\"user_id\":null,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"9fffce69-30ed-4c66-9ee9-e980e776cd57\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"9fffce69-30ed-4c66-9ee9-e980e776cd57\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"9fffce69-30ed-4c66-9ee9-e980e776cd57\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"9fffce69-30ed-4c66-9ee9-e980e776cd57\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"9fffce69-30ed-4c66-9ee9-e980e776cd57\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612561,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"9fffce69-30ed-4c66-9ee9-e980e776cd57\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [Prospect match] Cache miss, calling the API {\"identifier_type\":\"email\",\"identifier\":\"447700174614.447782589921.OeREojLVnk@txt.staging.jiminny.com\"} {\"correlation_id\":\"9fffce69-30ed-4c66-9ee9-e980e776cd57\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [Hubspot] Failed to fetch contact {\"email\":\"447700174614.447782589921.OeREojLVnk@txt.staging.jiminny.com\",\"reason\":\"[404] Client error: `GET https://api.hubapi.com/crm/v3/objects/contacts/447700174614.447782589921.OeREojLVnk%40txt.staging.jiminny.com?properties=email%2Cfirstname%2Clastname%2Ccountry%2Cphone%2Cmobilephone%2Cjobtitle%2Chubspot_owner_id%2Cassociatedcompanyid%2Cphoto&archived=0&idProperty=email` resulted in a `404 Not Found` response\"} {\"correlation_id\":\"9fffce69-30ed-4c66-9ee9-e980e776cd57\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [Prospect match] API returned empty result, caching the miss with empty prospect data {\"identifier_type\":\"email\",\"identifier\":\"447700174614.447782589921.OeREojLVnk@txt.staging.jiminny.com\"} {\"correlation_id\":\"9fffce69-30ed-4c66-9ee9-e980e776cd57\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"447700174614.447782589921.OeREojLVnk@txt.staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"9fffce69-30ed-4c66-9ee9-e980e776cd57\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612561,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"9fffce69-30ed-4c66-9ee9-e980e776cd57\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612561} {\"correlation_id\":\"9fffce69-30ed-4c66-9ee9-e980e776cd57\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612561,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"9fffce69-30ed-4c66-9ee9-e980e776cd57\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612561,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"9fffce69-30ed-4c66-9ee9-e980e776cd57\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612560,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"6a946453-4ffe-4df1-b91d-f73baf850a7a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612560} {\"correlation_id\":\"6a946453-4ffe-4df1-b91d-f73baf850a7a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612560,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"6a946453-4ffe-4df1-b91d-f73baf850a7a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612560,\"participants\":[{\"id\":999778,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":999779,\"user_id\":206,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"6a946453-4ffe-4df1-b91d-f73baf850a7a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"6a946453-4ffe-4df1-b91d-f73baf850a7a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"6a946453-4ffe-4df1-b91d-f73baf850a7a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"6a946453-4ffe-4df1-b91d-f73baf850a7a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"6a946453-4ffe-4df1-b91d-f73baf850a7a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"447782589921@txt.staging.jiminny.com\"} {\"correlation_id\":\"6a946453-4ffe-4df1-b91d-f73baf850a7a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"447782589921@txt.staging.jiminny.com\"} {\"correlation_id\":\"6a946453-4ffe-4df1-b91d-f73baf850a7a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"447782589921@txt.staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"6a946453-4ffe-4df1-b91d-f73baf850a7a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612560,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"6a946453-4ffe-4df1-b91d-f73baf850a7a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612560,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"6a946453-4ffe-4df1-b91d-f73baf850a7a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612560} {\"correlation_id\":\"6a946453-4ffe-4df1-b91d-f73baf850a7a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612560,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"6a946453-4ffe-4df1-b91d-f73baf850a7a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612560,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"6a946453-4ffe-4df1-b91d-f73baf850a7a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612559,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":5011,\"stage_id\":34}} {\"correlation_id\":\"6231af67-0852-4f81-a268-953b6db0b3aa\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612559} {\"correlation_id\":\"6231af67-0852-4f81-a268-953b6db0b3aa\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612559,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"6231af67-0852-4f81-a268-953b6db0b3aa\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612559,\"participants\":[{\"id\":999776,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999777,\"user_id\":null,\"contact_id\":97,\"lead_id\":null}]} {\"correlation_id\":\"6231af67-0852-4f81-a268-953b6db0b3aa\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"6231af67-0852-4f81-a268-953b6db0b3aa\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"6231af67-0852-4f81-a268-953b6db0b3aa\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"6231af67-0852-4f81-a268-953b6db0b3aa\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"6231af67-0852-4f81-a268-953b6db0b3aa\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612559,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"6231af67-0852-4f81-a268-953b6db0b3aa\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: ProspectCache - Searching DB for opportunity by owner {\"account_id\":69,\"contact_id\":97,\"owner_id\":206} {\"correlation_id\":\"6231af67-0852-4f81-a268-953b6db0b3aa\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: ProspectCache - Fallback DB opportunity search {\"account_id\":69,\"contact_id\":97} {\"correlation_id\":\"6231af67-0852-4f81-a268-953b6db0b3aa\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: ProspectCache - Opportunity DB search results {\"account_id\":69,\"contact_id\":97,\"opportunity_id\":5011} {\"correlation_id\":\"6231af67-0852-4f81-a268-953b6db0b3aa\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinsoncrusoe@test.com\"} {\"correlation_id\":\"6231af67-0852-4f81-a268-953b6db0b3aa\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612559,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"6231af67-0852-4f81-a268-953b6db0b3aa\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612559} {\"correlation_id\":\"6231af67-0852-4f81-a268-953b6db0b3aa\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612559,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"6231af67-0852-4f81-a268-953b6db0b3aa\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612559,\"remote_search\":true,\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":5011,\"stage_id\":34} {\"correlation_id\":\"6231af67-0852-4f81-a268-953b6db0b3aa\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612558,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":5011,\"stage_id\":34}} {\"correlation_id\":\"f8c4783d-18f2-45b6-a1b8-e9d80b788104\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612558} {\"correlation_id\":\"f8c4783d-18f2-45b6-a1b8-e9d80b788104\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612558,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"f8c4783d-18f2-45b6-a1b8-e9d80b788104\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:32] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612558,\"participants\":[{\"id\":999774,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999775,\"user_id\":null,\"contact_id\":97,\"lead_id\":null}]} {\"correlation_id\":\"f8c4783d-18f2-45b6-a1b8-e9d80b788104\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"f8c4783d-18f2-45b6-a1b8-e9d80b788104\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"f8c4783d-18f2-45b6-a1b8-e9d80b788104\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"f8c4783d-18f2-45b6-a1b8-e9d80b788104\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"f8c4783d-18f2-45b6-a1b8-e9d80b788104\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612558,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"f8c4783d-18f2-45b6-a1b8-e9d80b788104\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinsoncrusoe@test.com\"} {\"correlation_id\":\"f8c4783d-18f2-45b6-a1b8-e9d80b788104\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612558,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"f8c4783d-18f2-45b6-a1b8-e9d80b788104\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612558} {\"correlation_id\":\"f8c4783d-18f2-45b6-a1b8-e9d80b788104\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612558,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"f8c4783d-18f2-45b6-a1b8-e9d80b788104\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612558,\"remote_search\":true,\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":5011,\"stage_id\":34} {\"correlation_id\":\"f8c4783d-18f2-45b6-a1b8-e9d80b788104\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612557,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":5011,\"stage_id\":34}} {\"correlation_id\":\"d087a635-67a6-4fb4-be19-85f309031ebb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612557} {\"correlation_id\":\"d087a635-67a6-4fb4-be19-85f309031ebb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612557,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"d087a635-67a6-4fb4-be19-85f309031ebb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612557,\"participants\":[{\"id\":999772,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999773,\"user_id\":null,\"contact_id\":97,\"lead_id\":null}]} {\"correlation_id\":\"d087a635-67a6-4fb4-be19-85f309031ebb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"d087a635-67a6-4fb4-be19-85f309031ebb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"d087a635-67a6-4fb4-be19-85f309031ebb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d087a635-67a6-4fb4-be19-85f309031ebb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"d087a635-67a6-4fb4-be19-85f309031ebb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612557,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"d087a635-67a6-4fb4-be19-85f309031ebb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinsoncrusoe@test.com\"} {\"correlation_id\":\"d087a635-67a6-4fb4-be19-85f309031ebb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612557,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"d087a635-67a6-4fb4-be19-85f309031ebb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612557} {\"correlation_id\":\"d087a635-67a6-4fb4-be19-85f309031ebb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612557,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"d087a635-67a6-4fb4-be19-85f309031ebb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612557,\"remote_search\":true,\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":5011,\"stage_id\":34} {\"correlation_id\":\"d087a635-67a6-4fb4-be19-85f309031ebb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612556,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":5011,\"stage_id\":34}} {\"correlation_id\":\"462bb4b0-f644-4f69-b4a8-6030370c2202\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612556} {\"correlation_id\":\"462bb4b0-f644-4f69-b4a8-6030370c2202\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612556,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"462bb4b0-f644-4f69-b4a8-6030370c2202\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612556,\"participants\":[{\"id\":999770,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999771,\"user_id\":null,\"contact_id\":97,\"lead_id\":null}]} {\"correlation_id\":\"462bb4b0-f644-4f69-b4a8-6030370c2202\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"462bb4b0-f644-4f69-b4a8-6030370c2202\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"462bb4b0-f644-4f69-b4a8-6030370c2202\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"462bb4b0-f644-4f69-b4a8-6030370c2202\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"462bb4b0-f644-4f69-b4a8-6030370c2202\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612556,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"462bb4b0-f644-4f69-b4a8-6030370c2202\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinsoncrusoe@test.com\"} {\"correlation_id\":\"462bb4b0-f644-4f69-b4a8-6030370c2202\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612556,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"462bb4b0-f644-4f69-b4a8-6030370c2202\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612556} {\"correlation_id\":\"462bb4b0-f644-4f69-b4a8-6030370c2202\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612556,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"462bb4b0-f644-4f69-b4a8-6030370c2202\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612556,\"remote_search\":true,\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":5011,\"stage_id\":34} {\"correlation_id\":\"462bb4b0-f644-4f69-b4a8-6030370c2202\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612555,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":5011,\"stage_id\":34}} {\"correlation_id\":\"988eea33-83d6-41f7-93a3-9737c43ee6a3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612555} {\"correlation_id\":\"988eea33-83d6-41f7-93a3-9737c43ee6a3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [ EsUpdateProcessManager ] Finished updating entities in ES {\"worker\":\"\",\"peak_memory\":\"99.73 MB\",\"elapsed_seconds\":4.12,\"update_target\":\"activities\",\"should_iterate_again\":false} {\"correlation_id\":\"4ad05333-9afb-492e-9f0f-b2909ac45b32\",\"trace_id\":\"3d8feb24-b173-4158-b0a4-4cf33af85066\"}\n[2026-05-11 10:43:33] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612555,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"988eea33-83d6-41f7-93a3-9737c43ee6a3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612555,\"participants\":[{\"id\":999768,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999769,\"user_id\":null,\"contact_id\":97,\"lead_id\":null}]} {\"correlation_id\":\"988eea33-83d6-41f7-93a3-9737c43ee6a3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"988eea33-83d6-41f7-93a3-9737c43ee6a3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"988eea33-83d6-41f7-93a3-9737c43ee6a3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"988eea33-83d6-41f7-93a3-9737c43ee6a3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"988eea33-83d6-41f7-93a3-9737c43ee6a3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612555,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"988eea33-83d6-41f7-93a3-9737c43ee6a3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinsoncrusoe@test.com\"} {\"correlation_id\":\"988eea33-83d6-41f7-93a3-9737c43ee6a3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612555,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"988eea33-83d6-41f7-93a3-9737c43ee6a3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612555} {\"correlation_id\":\"988eea33-83d6-41f7-93a3-9737c43ee6a3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612555,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"988eea33-83d6-41f7-93a3-9737c43ee6a3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612555,\"remote_search\":true,\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":5011,\"stage_id\":34} {\"correlation_id\":\"988eea33-83d6-41f7-93a3-9737c43ee6a3\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612554,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":5011,\"stage_id\":34}} {\"correlation_id\":\"7ff43be6-a32a-412c-bb3d-1bd40aa30f65\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612554} {\"correlation_id\":\"7ff43be6-a32a-412c-bb3d-1bd40aa30f65\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612554,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"7ff43be6-a32a-412c-bb3d-1bd40aa30f65\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612554,\"participants\":[{\"id\":999766,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999767,\"user_id\":null,\"contact_id\":97,\"lead_id\":null}]} {\"correlation_id\":\"7ff43be6-a32a-412c-bb3d-1bd40aa30f65\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"7ff43be6-a32a-412c-bb3d-1bd40aa30f65\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"7ff43be6-a32a-412c-bb3d-1bd40aa30f65\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"7ff43be6-a32a-412c-bb3d-1bd40aa30f65\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"7ff43be6-a32a-412c-bb3d-1bd40aa30f65\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:33] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612554,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"7ff43be6-a32a-412c-bb3d-1bd40aa30f65\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinsoncrusoe@test.com\"} {\"correlation_id\":\"7ff43be6-a32a-412c-bb3d-1bd40aa30f65\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612554,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"7ff43be6-a32a-412c-bb3d-1bd40aa30f65\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612554} {\"correlation_id\":\"7ff43be6-a32a-412c-bb3d-1bd40aa30f65\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612554,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"7ff43be6-a32a-412c-bb3d-1bd40aa30f65\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612554,\"remote_search\":true,\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":5011,\"stage_id\":34} {\"correlation_id\":\"7ff43be6-a32a-412c-bb3d-1bd40aa30f65\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612553,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":5011,\"stage_id\":34}} {\"correlation_id\":\"3e6b8bf5-c432-4502-a026-aaf235b3b359\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612553} {\"correlation_id\":\"3e6b8bf5-c432-4502-a026-aaf235b3b359\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612553,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"3e6b8bf5-c432-4502-a026-aaf235b3b359\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612553,\"participants\":[{\"id\":999764,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999765,\"user_id\":null,\"contact_id\":97,\"lead_id\":null}]} {\"correlation_id\":\"3e6b8bf5-c432-4502-a026-aaf235b3b359\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"3e6b8bf5-c432-4502-a026-aaf235b3b359\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"3e6b8bf5-c432-4502-a026-aaf235b3b359\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"3e6b8bf5-c432-4502-a026-aaf235b3b359\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"3e6b8bf5-c432-4502-a026-aaf235b3b359\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612553,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"3e6b8bf5-c432-4502-a026-aaf235b3b359\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinsoncrusoe@test.com\"} {\"correlation_id\":\"3e6b8bf5-c432-4502-a026-aaf235b3b359\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612553,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"3e6b8bf5-c432-4502-a026-aaf235b3b359\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612553} {\"correlation_id\":\"3e6b8bf5-c432-4502-a026-aaf235b3b359\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612553,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"3e6b8bf5-c432-4502-a026-aaf235b3b359\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612553,\"remote_search\":true,\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":5011,\"stage_id\":34} {\"correlation_id\":\"3e6b8bf5-c432-4502-a026-aaf235b3b359\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"calendar:sync\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:34] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612552,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":5011,\"stage_id\":34}} {\"correlation_id\":\"1d70d062-f785-4c2c-81be-d992f1ef25c0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612552} {\"correlation_id\":\"1d70d062-f785-4c2c-81be-d992f1ef25c0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage before starting command {\"command\":\"mailbox:batch:retry-failed\",\"memoryBeforeCommandInMb\":60.0,\"memoryPeakBeforeCommandInMb\":99.727} {\"correlation_id\":\"c2dd1ea7-a2bd-44ee-95a4-60f88c2fcef7\",\"trace_id\":\"dcda0937-682f-46f0-bdc7-d2054de63c89\"}\n[2026-05-11 10:43:34] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612552,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"1d70d062-f785-4c2c-81be-d992f1ef25c0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612552,\"participants\":[{\"id\":999762,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999763,\"user_id\":null,\"contact_id\":97,\"lead_id\":null}]} {\"correlation_id\":\"1d70d062-f785-4c2c-81be-d992f1ef25c0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.NOTICE: Calendar sync start {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:34] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"1d70d062-f785-4c2c-81be-d992f1ef25c0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"1d70d062-f785-4c2c-81be-d992f1ef25c0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"1d70d062-f785-4c2c-81be-d992f1ef25c0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"1d70d062-f785-4c2c-81be-d992f1ef25c0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612552,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"1d70d062-f785-4c2c-81be-d992f1ef25c0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinsoncrusoe@test.com\"} {\"correlation_id\":\"1d70d062-f785-4c2c-81be-d992f1ef25c0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612552,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"1d70d062-f785-4c2c-81be-d992f1ef25c0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612552} {\"correlation_id\":\"1d70d062-f785-4c2c-81be-d992f1ef25c0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612552,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"1d70d062-f785-4c2c-81be-d992f1ef25c0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612552,\"remote_search\":true,\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":5011,\"stage_id\":34} {\"correlation_id\":\"1d70d062-f785-4c2c-81be-d992f1ef25c0\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"mailbox:batch:retry-failed\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":60.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"c2dd1ea7-a2bd-44ee-95a4-60f88c2fcef7\",\"trace_id\":\"dcda0937-682f-46f0-bdc7-d2054de63c89\"}\n[2026-05-11 10:43:34] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612551,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":5011,\"stage_id\":34}} {\"correlation_id\":\"35f5694e-2521-4089-a5f1-4adc747a40bf\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612551} {\"correlation_id\":\"35f5694e-2521-4089-a5f1-4adc747a40bf\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612551,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"35f5694e-2521-4089-a5f1-4adc747a40bf\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612551,\"participants\":[{\"id\":999760,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999761,\"user_id\":null,\"contact_id\":97,\"lead_id\":null}]} {\"correlation_id\":\"35f5694e-2521-4089-a5f1-4adc747a40bf\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"35f5694e-2521-4089-a5f1-4adc747a40bf\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"35f5694e-2521-4089-a5f1-4adc747a40bf\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"35f5694e-2521-4089-a5f1-4adc747a40bf\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"35f5694e-2521-4089-a5f1-4adc747a40bf\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612551,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"35f5694e-2521-4089-a5f1-4adc747a40bf\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinsoncrusoe@test.com\"} {\"correlation_id\":\"35f5694e-2521-4089-a5f1-4adc747a40bf\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612551,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"35f5694e-2521-4089-a5f1-4adc747a40bf\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612551} {\"correlation_id\":\"35f5694e-2521-4089-a5f1-4adc747a40bf\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612551,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"35f5694e-2521-4089-a5f1-4adc747a40bf\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612551,\"remote_search\":true,\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":5011,\"stage_id\":34} {\"correlation_id\":\"35f5694e-2521-4089-a5f1-4adc747a40bf\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1393,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:34] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1393,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:34] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612550,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":5011,\"stage_id\":34}} {\"correlation_id\":\"046f384b-a938-4138-a9e6-7df0ff6eb90b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:34] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612550} {\"correlation_id\":\"046f384b-a938-4138-a9e6-7df0ff6eb90b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612550,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"046f384b-a938-4138-a9e6-7df0ff6eb90b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1393,\"provider\":\"google\",\"refreshToken\":\"5aa7e2d96b53201cd16fca5d2e4ef3ad03320971fc064781d18aee3ae7b99fbf\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:34] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612550,\"participants\":[{\"id\":999758,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999759,\"user_id\":null,\"contact_id\":97,\"lead_id\":null}]} {\"correlation_id\":\"046f384b-a938-4138-a9e6-7df0ff6eb90b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"046f384b-a938-4138-a9e6-7df0ff6eb90b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"046f384b-a938-4138-a9e6-7df0ff6eb90b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"046f384b-a938-4138-a9e6-7df0ff6eb90b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"046f384b-a938-4138-a9e6-7df0ff6eb90b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612550,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"046f384b-a938-4138-a9e6-7df0ff6eb90b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinsoncrusoe@test.com\"} {\"correlation_id\":\"046f384b-a938-4138-a9e6-7df0ff6eb90b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612550,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"046f384b-a938-4138-a9e6-7df0ff6eb90b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612550} {\"correlation_id\":\"046f384b-a938-4138-a9e6-7df0ff6eb90b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612550,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"046f384b-a938-4138-a9e6-7df0ff6eb90b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612550,\"remote_search\":true,\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":5011,\"stage_id\":34} {\"correlation_id\":\"046f384b-a938-4138-a9e6-7df0ff6eb90b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612549,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":5011,\"stage_id\":34}} {\"correlation_id\":\"1fe4380c-c6ba-40c4-9bdc-65cf0a157cda\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612549} {\"correlation_id\":\"1fe4380c-c6ba-40c4-9bdc-65cf0a157cda\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612549,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"1fe4380c-c6ba-40c4-9bdc-65cf0a157cda\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612549,\"participants\":[{\"id\":999756,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999757,\"user_id\":null,\"contact_id\":97,\"lead_id\":null}]} {\"correlation_id\":\"1fe4380c-c6ba-40c4-9bdc-65cf0a157cda\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1393,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Account has been deleted\"}} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:34] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:34] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1393,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:34] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"1fe4380c-c6ba-40c4-9bdc-65cf0a157cda\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"1fe4380c-c6ba-40c4-9bdc-65cf0a157cda\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"1fe4380c-c6ba-40c4-9bdc-65cf0a157cda\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"1fe4380c-c6ba-40c4-9bdc-65cf0a157cda\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1387,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:34] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1387,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:34] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:34] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612549,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"1fe4380c-c6ba-40c4-9bdc-65cf0a157cda\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1387,\"provider\":\"google\",\"refreshToken\":\"8157ac6de94842937194009e9c50e459253600f799dacf6a40755ffdbeb5bba6\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:34] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinsoncrusoe@test.com\"} {\"correlation_id\":\"1fe4380c-c6ba-40c4-9bdc-65cf0a157cda\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612549,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"1fe4380c-c6ba-40c4-9bdc-65cf0a157cda\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612549} {\"correlation_id\":\"1fe4380c-c6ba-40c4-9bdc-65cf0a157cda\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612549,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"1fe4380c-c6ba-40c4-9bdc-65cf0a157cda\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612549,\"remote_search\":true,\"lead_id\":null,\"contact_id\":97,\"account_id\":69,\"opportunity_id\":5011,\"stage_id\":34} {\"correlation_id\":\"1fe4380c-c6ba-40c4-9bdc-65cf0a157cda\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612365,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"9f080e5f-9e25-468c-9d04-5cdf618dc528\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:34] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612365} {\"correlation_id\":\"9f080e5f-9e25-468c-9d04-5cdf618dc528\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612365,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"9f080e5f-9e25-468c-9d04-5cdf618dc528\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612365,\"participants\":[{\"id\":999563,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null},{\"id\":999564,\"user_id\":206,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"9f080e5f-9e25-468c-9d04-5cdf618dc528\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"9f080e5f-9e25-468c-9d04-5cdf618dc528\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"9f080e5f-9e25-468c-9d04-5cdf618dc528\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"9f080e5f-9e25-468c-9d04-5cdf618dc528\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1387,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Account has been deleted\"}} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"9f080e5f-9e25-468c-9d04-5cdf618dc528\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1387,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"9f080e5f-9e25-468c-9d04-5cdf618dc528\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612365,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"9f080e5f-9e25-468c-9d04-5cdf618dc528\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1348,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612365,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"9f080e5f-9e25-468c-9d04-5cdf618dc528\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1348,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612365} {\"correlation_id\":\"9f080e5f-9e25-468c-9d04-5cdf618dc528\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612365,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"9f080e5f-9e25-468c-9d04-5cdf618dc528\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1348,\"provider\":\"google\",\"refreshToken\":\"9e7d13d3032d0cb1b79d8e95aef01383e8e91eb52ff8ee960c8a0b6b95cd8c73\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612365,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"9f080e5f-9e25-468c-9d04-5cdf618dc528\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612360,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"e1af9830-ccba-44db-a1a0-d9ac3ec0da4d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612360} {\"correlation_id\":\"e1af9830-ccba-44db-a1a0-d9ac3ec0da4d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612360,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"e1af9830-ccba-44db-a1a0-d9ac3ec0da4d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612360,\"participants\":[{\"id\":999552,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null},{\"id\":999553,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999565,\"user_id\":null,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"e1af9830-ccba-44db-a1a0-d9ac3ec0da4d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"e1af9830-ccba-44db-a1a0-d9ac3ec0da4d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"e1af9830-ccba-44db-a1a0-d9ac3ec0da4d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"e1af9830-ccba-44db-a1a0-d9ac3ec0da4d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"e1af9830-ccba-44db-a1a0-d9ac3ec0da4d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"e1af9830-ccba-44db-a1a0-d9ac3ec0da4d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612360,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"e1af9830-ccba-44db-a1a0-d9ac3ec0da4d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1348,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Bad Request\"}} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1348,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1361,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1361,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1361,\"provider\":\"google\",\"refreshToken\":\"6c843da199c2b9907445329304fcc4ec5057a4ee748d8299641764395c08e1fd\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1361,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Account has been deleted\"}} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1361,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [Hubspot] Pagination completed {\"team_id\":2,\"endpoint\":\"https://api.hubapi.com/crm/v3/objects/contact/search\",\"total_requests\":1,\"total_records_fetched\":0,\"total_elapsed_seconds\":0.24,\"average_seconds_per_request\":0.24} {\"correlation_id\":\"e1af9830-ccba-44db-a1a0-d9ac3ec0da4d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612360,\"participants_processed\":3,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"e1af9830-ccba-44db-a1a0-d9ac3ec0da4d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1310,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612360} {\"correlation_id\":\"e1af9830-ccba-44db-a1a0-d9ac3ec0da4d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1310,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612360,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"e1af9830-ccba-44db-a1a0-d9ac3ec0da4d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1310,\"provider\":\"google\",\"refreshToken\":\"e34818922c2830a660813a63f6169a4a9a992ae2cccd7dc8dd7796cfdb470ef1\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612360,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"e1af9830-ccba-44db-a1a0-d9ac3ec0da4d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612340,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"eb3baeb0-9f99-4e3d-98f1-fdc0d69023fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612340} {\"correlation_id\":\"eb3baeb0-9f99-4e3d-98f1-fdc0d69023fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612340,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"eb3baeb0-9f99-4e3d-98f1-fdc0d69023fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612340,\"participants\":[{\"id\":999516,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null},{\"id\":999517,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999518,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":999519,\"user_id\":null,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"eb3baeb0-9f99-4e3d-98f1-fdc0d69023fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"eb3baeb0-9f99-4e3d-98f1-fdc0d69023fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"eb3baeb0-9f99-4e3d-98f1-fdc0d69023fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"eb3baeb0-9f99-4e3d-98f1-fdc0d69023fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"eb3baeb0-9f99-4e3d-98f1-fdc0d69023fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"eb3baeb0-9f99-4e3d-98f1-fdc0d69023fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612340,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"eb3baeb0-9f99-4e3d-98f1-fdc0d69023fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1310,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Bad Request\"}} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1310,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1333,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1333,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1333,\"provider\":\"google\",\"refreshToken\":\"6c902986546d8e8da1dc539b046cdc1d458f519acc972e5b5f1d6a1a295165e0\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [Hubspot] Pagination completed {\"team_id\":2,\"endpoint\":\"https://api.hubapi.com/crm/v3/objects/contact/search\",\"total_requests\":1,\"total_records_fetched\":0,\"total_elapsed_seconds\":0.22,\"average_seconds_per_request\":0.22} {\"correlation_id\":\"eb3baeb0-9f99-4e3d-98f1-fdc0d69023fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612340,\"participants_processed\":4,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"eb3baeb0-9f99-4e3d-98f1-fdc0d69023fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612340} {\"correlation_id\":\"eb3baeb0-9f99-4e3d-98f1-fdc0d69023fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612340,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"eb3baeb0-9f99-4e3d-98f1-fdc0d69023fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612340,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"eb3baeb0-9f99-4e3d-98f1-fdc0d69023fb\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612339,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"68ce2e50-cf66-4aab-9ce0-ab242e6027c4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612339} {\"correlation_id\":\"68ce2e50-cf66-4aab-9ce0-ab242e6027c4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612339,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"68ce2e50-cf66-4aab-9ce0-ab242e6027c4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612339,\"participants\":[{\"id\":999514,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null},{\"id\":999515,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999540,\"user_id\":null,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"68ce2e50-cf66-4aab-9ce0-ab242e6027c4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"68ce2e50-cf66-4aab-9ce0-ab242e6027c4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"68ce2e50-cf66-4aab-9ce0-ab242e6027c4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"68ce2e50-cf66-4aab-9ce0-ab242e6027c4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"68ce2e50-cf66-4aab-9ce0-ab242e6027c4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"68ce2e50-cf66-4aab-9ce0-ab242e6027c4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612339,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"68ce2e50-cf66-4aab-9ce0-ab242e6027c4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612339,\"participants_processed\":3,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"68ce2e50-cf66-4aab-9ce0-ab242e6027c4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1333,\"provider\":\"google\",\"responseBody\":{\"error\":\"unauthorized_client\",\"error_description\":\"Unauthorized\"}} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612339} {\"correlation_id\":\"68ce2e50-cf66-4aab-9ce0-ab242e6027c4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1333,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612339,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"68ce2e50-cf66-4aab-9ce0-ab242e6027c4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612339,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"68ce2e50-cf66-4aab-9ce0-ab242e6027c4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1368,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1368,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1368,\"provider\":\"google\",\"refreshToken\":\"d2f128898ff8543bd16b69cfae37896ab85119b0f5ed2b431d739593bb600333\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612336,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36}} {\"correlation_id\":\"ffc0ea4a-744d-49a4-a9ad-736fbda03384\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612336} {\"correlation_id\":\"ffc0ea4a-744d-49a4-a9ad-736fbda03384\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612336,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"ffc0ea4a-744d-49a4-a9ad-736fbda03384\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612336,\"participants\":[{\"id\":999508,\"user_id\":null,\"contact_id\":4491,\"lead_id\":null},{\"id\":999509,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":999512,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":999513,\"user_id\":null,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"ffc0ea4a-744d-49a4-a9ad-736fbda03384\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"ffc0ea4a-744d-49a4-a9ad-736fbda03384\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"ffc0ea4a-744d-49a4-a9ad-736fbda03384\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"ffc0ea4a-744d-49a4-a9ad-736fbda03384\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"ffc0ea4a-744d-49a4-a9ad-736fbda03384\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"horencho@gmail.com\"} {\"correlation_id\":\"ffc0ea4a-744d-49a4-a9ad-736fbda03384\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612336,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"ffc0ea4a-744d-49a4-a9ad-736fbda03384\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612336,\"participants_processed\":4,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"ffc0ea4a-744d-49a4-a9ad-736fbda03384\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612336} {\"correlation_id\":\"ffc0ea4a-744d-49a4-a9ad-736fbda03384\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612336,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"ffc0ea4a-744d-49a4-a9ad-736fbda03384\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612336,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4491,\"account_id\":243,\"opportunity_id\":276,\"stage_id\":36} {\"correlation_id\":\"ffc0ea4a-744d-49a4-a9ad-736fbda03384\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612183,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36}} {\"correlation_id\":\"48880be8-e032-41b2-948e-c4ec99247548\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612183} {\"correlation_id\":\"48880be8-e032-41b2-948e-c4ec99247548\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612183,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"48880be8-e032-41b2-948e-c4ec99247548\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612183,\"participants\":[{\"id\":999227,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null},{\"id\":999228,\"user_id\":261,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"48880be8-e032-41b2-948e-c4ec99247548\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"48880be8-e032-41b2-948e-c4ec99247548\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"48880be8-e032-41b2-948e-c4ec99247548\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"48880be8-e032-41b2-948e-c4ec99247548\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"48880be8-e032-41b2-948e-c4ec99247548\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"48880be8-e032-41b2-948e-c4ec99247548\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612183,\"team_id\":2,\"email\":\"adelina.petrova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"48880be8-e032-41b2-948e-c4ec99247548\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612183,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"48880be8-e032-41b2-948e-c4ec99247548\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612183} {\"correlation_id\":\"48880be8-e032-41b2-948e-c4ec99247548\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612183,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"48880be8-e032-41b2-948e-c4ec99247548\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612183,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36} {\"correlation_id\":\"48880be8-e032-41b2-948e-c4ec99247548\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612182,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36}} {\"correlation_id\":\"3858f13d-426c-474e-98cf-11576b98e5dd\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612182} {\"correlation_id\":\"3858f13d-426c-474e-98cf-11576b98e5dd\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612182,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"3858f13d-426c-474e-98cf-11576b98e5dd\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612182,\"participants\":[{\"id\":999225,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null},{\"id\":999226,\"user_id\":261,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"3858f13d-426c-474e-98cf-11576b98e5dd\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"3858f13d-426c-474e-98cf-11576b98e5dd\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"3858f13d-426c-474e-98cf-11576b98e5dd\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"3858f13d-426c-474e-98cf-11576b98e5dd\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"3858f13d-426c-474e-98cf-11576b98e5dd\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"3858f13d-426c-474e-98cf-11576b98e5dd\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612182,\"team_id\":2,\"email\":\"adelina.petrova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"3858f13d-426c-474e-98cf-11576b98e5dd\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612182,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"3858f13d-426c-474e-98cf-11576b98e5dd\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612182} {\"correlation_id\":\"3858f13d-426c-474e-98cf-11576b98e5dd\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612182,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"3858f13d-426c-474e-98cf-11576b98e5dd\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612182,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36} {\"correlation_id\":\"3858f13d-426c-474e-98cf-11576b98e5dd\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612181,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36}} {\"correlation_id\":\"33fd8677-950a-4069-9642-511743b8b619\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612181} {\"correlation_id\":\"33fd8677-950a-4069-9642-511743b8b619\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612181,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"33fd8677-950a-4069-9642-511743b8b619\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612181,\"participants\":[{\"id\":999223,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null},{\"id\":999224,\"user_id\":261,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"33fd8677-950a-4069-9642-511743b8b619\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"33fd8677-950a-4069-9642-511743b8b619\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"33fd8677-950a-4069-9642-511743b8b619\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"33fd8677-950a-4069-9642-511743b8b619\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"33fd8677-950a-4069-9642-511743b8b619\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"33fd8677-950a-4069-9642-511743b8b619\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1368,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Bad Request\"}} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612181,\"team_id\":2,\"email\":\"adelina.petrova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"33fd8677-950a-4069-9642-511743b8b619\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1368,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612181,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"33fd8677-950a-4069-9642-511743b8b619\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612181} {\"correlation_id\":\"33fd8677-950a-4069-9642-511743b8b619\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612181,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"33fd8677-950a-4069-9642-511743b8b619\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612181,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36} {\"correlation_id\":\"33fd8677-950a-4069-9642-511743b8b619\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1365,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:35] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1365,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1365,\"provider\":\"google\",\"refreshToken\":\"7676e4a9afcd082b413248ab5ec6e487021fec6a9bdf315860a59cefad9caad8\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":612180,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36}} {\"correlation_id\":\"dc184f1d-d7e2-4bc1-92a0-c572af69ba86\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612180} {\"correlation_id\":\"dc184f1d-d7e2-4bc1-92a0-c572af69ba86\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612180,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"dc184f1d-d7e2-4bc1-92a0-c572af69ba86\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":612180,\"participants\":[{\"id\":999221,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null},{\"id\":999222,\"user_id\":261,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"dc184f1d-d7e2-4bc1-92a0-c572af69ba86\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"dc184f1d-d7e2-4bc1-92a0-c572af69ba86\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"dc184f1d-d7e2-4bc1-92a0-c572af69ba86\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"dc184f1d-d7e2-4bc1-92a0-c572af69ba86\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"dc184f1d-d7e2-4bc1-92a0-c572af69ba86\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"dc184f1d-d7e2-4bc1-92a0-c572af69ba86\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":612180,\"team_id\":2,\"email\":\"adelina.petrova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"dc184f1d-d7e2-4bc1-92a0-c572af69ba86\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":612180,\"participants_processed\":2,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"dc184f1d-d7e2-4bc1-92a0-c572af69ba86\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":612180} {\"correlation_id\":\"dc184f1d-d7e2-4bc1-92a0-c572af69ba86\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":612180,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"dc184f1d-d7e2-4bc1-92a0-c572af69ba86\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":612180,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":299,\"stage_id\":36} {\"correlation_id\":\"dc184f1d-d7e2-4bc1-92a0-c572af69ba86\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":611455,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"8f14fe33-00cc-4910-996e-efa541fc5bb7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":611455} {\"correlation_id\":\"8f14fe33-00cc-4910-996e-efa541fc5bb7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":611455,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"8f14fe33-00cc-4910-996e-efa541fc5bb7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":611455,\"participants\":[{\"id\":997961,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":997962,\"user_id\":1460,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"8f14fe33-00cc-4910-996e-efa541fc5bb7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"8f14fe33-00cc-4910-996e-efa541fc5bb7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"8f14fe33-00cc-4910-996e-efa541fc5bb7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"8f14fe33-00cc-4910-996e-efa541fc5bb7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"8f14fe33-00cc-4910-996e-efa541fc5bb7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [Prospect match] Cache miss, calling the API {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"8f14fe33-00cc-4910-996e-efa541fc5bb7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1365,\"provider\":\"google\",\"responseBody\":{\"error\":\"unauthorized_client\",\"error_description\":\"Unauthorized\"}} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1365,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1364,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1364,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1364,\"provider\":\"google\",\"refreshToken\":\"dd5882ebce76e645292ce33ae74238abbb77c0a4ecc6a2bfe723cad82e72ba8e\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.INFO: [Hubspot] Failed to fetch contact {\"email\":\"support@staging.jiminny.com\",\"reason\":\"[404] Client error: `GET https://api.hubapi.com/crm/v3/objects/contacts/support%40staging.jiminny.com?properties=email%2Cfirstname%2Clastname%2Ccountry%2Cphone%2Cmobilephone%2Cjobtitle%2Chubspot_owner_id%2Cassociatedcompanyid%2Cphoto&archived=0&idProperty=email` resulted in a `404 Not Found` response\"} {\"correlation_id\":\"8f14fe33-00cc-4910-996e-efa541fc5bb7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [Prospect match] API returned empty result, caching the miss with empty prospect data {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"8f14fe33-00cc-4910-996e-efa541fc5bb7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1364,\"provider\":\"google\",\"responseBody\":{\"error\":\"unauthorized_client\",\"error_description\":\"Unauthorized\"}} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1364,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1370,\"provider\":\"office\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1370,\"provider\":\"office\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1370,\"provider\":\"office\",\"refreshToken\":\"b7ee8035306d0043cea6e00e7c4fe14f745e44074a1194db62a31cdf8b70af3e\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.INFO: [Hubspot] Pagination completed {\"team_id\":2,\"endpoint\":\"https://api.hubapi.com/crm/v3/objects/contact/search\",\"total_requests\":1,\"total_records_fetched\":0,\"total_elapsed_seconds\":0.21,\"average_seconds_per_request\":0.21} {\"correlation_id\":\"8f14fe33-00cc-4910-996e-efa541fc5bb7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"8f14fe33-00cc-4910-996e-efa541fc5bb7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [Prospect match] Cache miss {\"identifier_type\":\"domain\",\"identifier\":\"jiminny.com\",\"crm\":\"hubspot\"} {\"correlation_id\":\"8f14fe33-00cc-4910-996e-efa541fc5bb7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [Prospect match] Cache miss, calling the API {\"identifier_type\":\"domain\",\"identifier\":\"jiminny.com\"} {\"correlation_id\":\"8f14fe33-00cc-4910-996e-efa541fc5bb7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1370,\"provider\":\"office\",\"responseBody\":\"{\\\"error\\\":\\\"invalid_client\\\",\\\"error_description\\\":\\\"AADSTS7000215: Invalid client secret provided. Ensure the secret being sent in the request is the client secret value, not the client secret ID, for a secret added to app 'bbcbb2ef-6200-4fae-82bd-d81f5dd738da'. Trace ID: 329b429c-bad8-4f19-bace-40e48a111900 Correlation ID: 4f12c9af-e118-468c-8441-ebc325071d4b Timestamp: 2026-05-11 10:43:36Z\\\",\\\"error_codes\\\":[7000215],\\\"timestamp\\\":\\\"2026-05-11 10:43:36Z\\\",\\\"trace_id\\\":\\\"329b429c-bad8-4f19-bace-40e48a111900\\\",\\\"correlation_id\\\":\\\"4f12c9af-e118-468c-8441-ebc325071d4b\\\",\\\"error_uri\\\":\\\"https://login.microsoftonline.com/error?code=7000215\\\"}\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1370,\"provider\":\"office\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1202,\"provider\":\"office\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1202,\"provider\":\"office\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1202,\"provider\":\"office\",\"refreshToken\":\"b458799ccc29b21a6e2eb5260fdb63e49ccba21bf942a3973fb63799bd7f0afe\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:36] local.INFO: [HubSpot] importAccount {\"crm_provider_id\":\"749766179\",\"config_id\":2} {\"correlation_id\":\"8f14fe33-00cc-4910-996e-efa541fc5bb7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:36] local.INFO: [HubSpot] CRM Search requested {\"request\":{\"filterGroups\":[{\"filters\":[{\"propertyName\":\"associations.company\",\"operator\":\"EQ\",\"value\":\"749766179\"},{\"propertyName\":\"dealstage\",\"operator\":\"NOT_IN\",\"values\":[\"closedwon\",\"4040964\",\"59247967\"]},{\"propertyName\":\"dealstage\",\"operator\":\"NOT_IN\",\"values\":[\"closedlost\",\"4040965\",\"59247968\"]}]}],\"sorts\":[{\"propertyName\":\"modifieddate\",\"direction\":\"DESCENDING\"}],\"properties\":[\"dealname\",\"amount\",\"hubspot_owner_id\",\"pipeline\",\"dealstage\",\"closedate\",\"deal_currency_code\"],\"limit\":200}} {\"correlation_id\":\"8f14fe33-00cc-4910-996e-efa541fc5bb7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:37] local.INFO: [Hubspot] Pagination completed {\"team_id\":2,\"endpoint\":\"https://api.hubapi.com/crm/v3/objects/deals/search\",\"total_requests\":1,\"total_records_fetched\":10,\"total_elapsed_seconds\":0.24,\"average_seconds_per_request\":0.24} {\"correlation_id\":\"8f14fe33-00cc-4910-996e-efa541fc5bb7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:37] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":611455,\"team_id\":2,\"email\":\"aneliya.angelova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"8f14fe33-00cc-4910-996e-efa541fc5bb7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:37] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":611455,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"8f14fe33-00cc-4910-996e-efa541fc5bb7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:37] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":611455} {\"correlation_id\":\"8f14fe33-00cc-4910-996e-efa541fc5bb7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:37] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":611455,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"8f14fe33-00cc-4910-996e-efa541fc5bb7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:37] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":611455,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"8f14fe33-00cc-4910-996e-efa541fc5bb7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:37] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":611451,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"7ea49a4a-ec21-4b93-b990-d1e79531b72a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:37] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":611451} {\"correlation_id\":\"7ea49a4a-ec21-4b93-b990-d1e79531b72a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:37] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":611451,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"7ea49a4a-ec21-4b93-b990-d1e79531b72a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:37] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":611451,\"participants\":[{\"id\":997955,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":997956,\"user_id\":18,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"7ea49a4a-ec21-4b93-b990-d1e79531b72a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:37] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"7ea49a4a-ec21-4b93-b990-d1e79531b72a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:37] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"7ea49a4a-ec21-4b93-b990-d1e79531b72a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:37] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"7ea49a4a-ec21-4b93-b990-d1e79531b72a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:37] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"7ea49a4a-ec21-4b93-b990-d1e79531b72a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:37] local.INFO: [Prospect match] Cache miss, calling the API {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"7ea49a4a-ec21-4b93-b990-d1e79531b72a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:38] local.INFO: [Hubspot] Failed to fetch contact {\"email\":\"support@staging.jiminny.com\",\"reason\":\"[404] Client error: `GET https://api.hubapi.com/crm/v3/objects/contacts/support%40staging.jiminny.com?properties=email%2Cfirstname%2Clastname%2Ccountry%2Cphone%2Cmobilephone%2Cjobtitle%2Chubspot_owner_id%2Cassociatedcompanyid%2Cphoto&archived=0&idProperty=email` resulted in a `404 Not Found` response\"} {\"correlation_id\":\"7ea49a4a-ec21-4b93-b990-d1e79531b72a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:38] local.INFO: [Prospect match] API returned empty result, caching the miss with empty prospect data {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"7ea49a4a-ec21-4b93-b990-d1e79531b72a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:38] local.INFO: [Hubspot] Pagination completed {\"team_id\":2,\"endpoint\":\"https://api.hubapi.com/crm/v3/objects/contact/search\",\"total_requests\":1,\"total_records_fetched\":0,\"total_elapsed_seconds\":0.22,\"average_seconds_per_request\":0.22} {\"correlation_id\":\"7ea49a4a-ec21-4b93-b990-d1e79531b72a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:38] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"7ea49a4a-ec21-4b93-b990-d1e79531b72a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:38] local.INFO: [Prospect match] Cache miss {\"identifier_type\":\"domain\",\"identifier\":\"jiminny.com\",\"crm\":\"hubspot\"} {\"correlation_id\":\"7ea49a4a-ec21-4b93-b990-d1e79531b72a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:38] local.INFO: [Prospect match] Cache miss, calling the API {\"identifier_type\":\"domain\",\"identifier\":\"jiminny.com\"} {\"correlation_id\":\"7ea49a4a-ec21-4b93-b990-d1e79531b72a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:38] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1202,\"provider\":\"office\",\"responseBody\":\"{\\\"error\\\":\\\"invalid_client\\\",\\\"error_description\\\":\\\"AADSTS7000215: Invalid client secret provided. Ensure the secret being sent in the request is the client secret value, not the client secret ID, for a secret added to app 'bbcbb2ef-6200-4fae-82bd-d81f5dd738da'. Trace ID: e8a4c6ea-4ab7-4915-b7a3-93868da51100 Correlation ID: e670afc0-d33c-4205-924c-76bda5c3d01d Timestamp: 2026-05-11 10:43:38Z\\\",\\\"error_codes\\\":[7000215],\\\"timestamp\\\":\\\"2026-05-11 10:43:38Z\\\",\\\"trace_id\\\":\\\"e8a4c6ea-4ab7-4915-b7a3-93868da51100\\\",\\\"correlation_id\\\":\\\"e670afc0-d33c-4205-924c-76bda5c3d01d\\\",\\\"error_uri\\\":\\\"https://login.microsoftonline.com/error?code=7000215\\\"}\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:38] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:38] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1202,\"provider\":\"office\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:38] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1502,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:38] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1502,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:38] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:38] local.INFO: Calendar sync job dispatched {\"calendar_id\":501} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:38] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1300,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:38] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1300,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:38] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:38] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1300,\"provider\":\"google\",\"refreshToken\":\"4b811db0725fd9602a95943519a7da935e2a5065da7d9ebfcb170752e3e1ddb8\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:38] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1300,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Account has been deleted\"}} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:38] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:38] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1300,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:38] local.INFO: [HubSpot] importAccount {\"crm_provider_id\":\"749766179\",\"config_id\":2} {\"correlation_id\":\"7ea49a4a-ec21-4b93-b990-d1e79531b72a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:38] local.INFO: [HubSpot] CRM Search requested {\"request\":{\"filterGroups\":[{\"filters\":[{\"propertyName\":\"associations.company\",\"operator\":\"EQ\",\"value\":\"749766179\"},{\"propertyName\":\"dealstage\",\"operator\":\"NOT_IN\",\"values\":[\"closedwon\",\"4040964\",\"59247967\"]},{\"propertyName\":\"dealstage\",\"operator\":\"NOT_IN\",\"values\":[\"closedlost\",\"4040965\",\"59247968\"]}]}],\"sorts\":[{\"propertyName\":\"modifieddate\",\"direction\":\"DESCENDING\"}],\"properties\":[\"dealname\",\"amount\",\"hubspot_owner_id\",\"pipeline\",\"dealstage\",\"closedate\",\"deal_currency_code\"],\"limit\":200}} {\"correlation_id\":\"7ea49a4a-ec21-4b93-b990-d1e79531b72a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:38] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1409,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:38] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1409,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:38] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:38] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1409,\"provider\":\"google\",\"refreshToken\":\"e2a3f2d06894894eed1ee87d9db1ace77d4d42ee6e1288a8940ad2c10333b0c4\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:38] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1409,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Bad Request\"}} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:38] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:38] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1409,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:39] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1352,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:39] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1352,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:39] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:39] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1352,\"provider\":\"google\",\"refreshToken\":\"dd4b16b00fdc1216da6b717c02338c073636e29162826b2de6db3f064fc029eb\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:39] local.INFO: [Hubspot] Pagination completed {\"team_id\":2,\"endpoint\":\"https://api.hubapi.com/crm/v3/objects/deals/search\",\"total_requests\":1,\"total_records_fetched\":10,\"total_elapsed_seconds\":0.28,\"average_seconds_per_request\":0.28} {\"correlation_id\":\"7ea49a4a-ec21-4b93-b990-d1e79531b72a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1352,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Token has been expired or revoked.\"}} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:39] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:39] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1352,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:39] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1296,\"provider\":\"office\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:39] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1296,\"provider\":\"office\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:39] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:39] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1296,\"provider\":\"office\",\"refreshToken\":\"011ae723c9d800c674e0b4be76f49fc046dac7d501b66c59ef0d9549cfa56ae5\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:39] local.INFO: [ EsUpdateProcessManager ] Finished updating entities in ES {\"worker\":\"\",\"peak_memory\":\"99.73 MB\",\"elapsed_seconds\":0.9,\"update_target\":\"activities\",\"should_iterate_again\":false} {\"correlation_id\":\"4ad05333-9afb-492e-9f0f-b2909ac45b32\",\"trace_id\":\"3d8feb24-b173-4158-b0a4-4cf33af85066\"}\n[2026-05-11 10:43:39] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":611451,\"team_id\":2,\"email\":\"veselin.kulov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"7ea49a4a-ec21-4b93-b990-d1e79531b72a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":611451,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"7ea49a4a-ec21-4b93-b990-d1e79531b72a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":611451} {\"correlation_id\":\"7ea49a4a-ec21-4b93-b990-d1e79531b72a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":611451,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"7ea49a4a-ec21-4b93-b990-d1e79531b72a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":611451,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"7ea49a4a-ec21-4b93-b990-d1e79531b72a\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":611087,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"02dbf6fa-a774-4090-af49-cabec99363d4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":611087} {\"correlation_id\":\"02dbf6fa-a774-4090-af49-cabec99363d4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":611087,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"02dbf6fa-a774-4090-af49-cabec99363d4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":611087,\"participants\":[{\"id\":997368,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":997369,\"user_id\":18,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"02dbf6fa-a774-4090-af49-cabec99363d4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"02dbf6fa-a774-4090-af49-cabec99363d4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"02dbf6fa-a774-4090-af49-cabec99363d4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"02dbf6fa-a774-4090-af49-cabec99363d4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"02dbf6fa-a774-4090-af49-cabec99363d4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"02dbf6fa-a774-4090-af49-cabec99363d4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"02dbf6fa-a774-4090-af49-cabec99363d4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"02dbf6fa-a774-4090-af49-cabec99363d4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":611087,\"team_id\":2,\"email\":\"veselin.kulov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"02dbf6fa-a774-4090-af49-cabec99363d4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":611087,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"02dbf6fa-a774-4090-af49-cabec99363d4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":611087} {\"correlation_id\":\"02dbf6fa-a774-4090-af49-cabec99363d4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":611087,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"02dbf6fa-a774-4090-af49-cabec99363d4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":611087,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"02dbf6fa-a774-4090-af49-cabec99363d4\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1296,\"provider\":\"office\",\"responseBody\":\"{\\\"error\\\":\\\"invalid_client\\\",\\\"error_description\\\":\\\"AADSTS7000215: Invalid client secret provided. Ensure the secret being sent in the request is the client secret value, not the client secret ID, for a secret added to app 'bbcbb2ef-6200-4fae-82bd-d81f5dd738da'. Trace ID: 4ff43bd2-d427-4049-964c-57fbcad91a00 Correlation ID: 6e92626f-a36d-4c41-ae7c-3b7296df0110 Timestamp: 2026-05-11 10:43:39Z\\\",\\\"error_codes\\\":[7000215],\\\"timestamp\\\":\\\"2026-05-11 10:43:39Z\\\",\\\"trace_id\\\":\\\"4ff43bd2-d427-4049-964c-57fbcad91a00\\\",\\\"correlation_id\\\":\\\"6e92626f-a36d-4c41-ae7c-3b7296df0110\\\",\\\"error_uri\\\":\\\"https://login.microsoftonline.com/error?code=7000215\\\"}\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:39] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:39] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1296,\"provider\":\"office\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:39] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":611076,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"ae1d456f-4b46-4c79-bb7f-bc2e95d48c9f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":611076} {\"correlation_id\":\"ae1d456f-4b46-4c79-bb7f-bc2e95d48c9f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":611076,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"ae1d456f-4b46-4c79-bb7f-bc2e95d48c9f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":611076,\"participants\":[{\"id\":997346,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":997347,\"user_id\":18,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"ae1d456f-4b46-4c79-bb7f-bc2e95d48c9f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":391,\"provider\":\"office\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:39] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":391,\"provider\":\"office\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:39] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:39] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":391,\"provider\":\"office\",\"refreshToken\":\"00045eebae0f39b34887c6d53f92ae78064f7145e1f4b67754aebd03cfb2d881\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:39] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"ae1d456f-4b46-4c79-bb7f-bc2e95d48c9f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"ae1d456f-4b46-4c79-bb7f-bc2e95d48c9f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"ae1d456f-4b46-4c79-bb7f-bc2e95d48c9f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"ae1d456f-4b46-4c79-bb7f-bc2e95d48c9f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:39] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"ae1d456f-4b46-4c79-bb7f-bc2e95d48c9f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"ae1d456f-4b46-4c79-bb7f-bc2e95d48c9f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"ae1d456f-4b46-4c79-bb7f-bc2e95d48c9f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":611076,\"team_id\":2,\"email\":\"veselin.kulov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"ae1d456f-4b46-4c79-bb7f-bc2e95d48c9f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":611076,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"ae1d456f-4b46-4c79-bb7f-bc2e95d48c9f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":611076} {\"correlation_id\":\"ae1d456f-4b46-4c79-bb7f-bc2e95d48c9f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":611076,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"ae1d456f-4b46-4c79-bb7f-bc2e95d48c9f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":611076,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"ae1d456f-4b46-4c79-bb7f-bc2e95d48c9f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1502,\"provider\":\"google\"} {\"correlation_id\":\"0fdd7dce-04e2-4696-ba87-8089a739c81c\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1502,\"provider\":\"google\"} {\"correlation_id\":\"0fdd7dce-04e2-4696-ba87-8089a739c81c\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:40] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"0fdd7dce-04e2-4696-ba87-8089a739c81c\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:40] local.INFO: [Calendar] Processing sync {\"calendarId\":\"a33076c1-8d97-431a-99f0-85c9524e118b\",\"from\":null,\"to\":null,\"delta\":\"CIiFh8TP44kDEIiFh8TP44kDGAUgkZvkzgIokZvkzgI=\",\"last_sync\":\"2024-12-09 07:12:53\",\"dateMode\":\"daily\"} {\"correlation_id\":\"0fdd7dce-04e2-4696-ba87-8089a739c81c\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:40] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"integration-app\",\"crm_owner\":1695,\"team_id\":3143} {\"correlation_id\":\"0fdd7dce-04e2-4696-ba87-8089a739c81c\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1502,\"provider\":\"google\"} {\"correlation_id\":\"0fdd7dce-04e2-4696-ba87-8089a739c81c\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1502,\"provider\":\"google\"} {\"correlation_id\":\"0fdd7dce-04e2-4696-ba87-8089a739c81c\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:40] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"0fdd7dce-04e2-4696-ba87-8089a739c81c\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":610935,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"9f5579a5-e89f-4074-9843-d5c1f123878d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610935} {\"correlation_id\":\"9f5579a5-e89f-4074-9843-d5c1f123878d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610935,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"9f5579a5-e89f-4074-9843-d5c1f123878d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":610935,\"participants\":[{\"id\":997141,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":997142,\"user_id\":18,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"9f5579a5-e89f-4074-9843-d5c1f123878d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"9f5579a5-e89f-4074-9843-d5c1f123878d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"9f5579a5-e89f-4074-9843-d5c1f123878d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"9f5579a5-e89f-4074-9843-d5c1f123878d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"9f5579a5-e89f-4074-9843-d5c1f123878d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"9f5579a5-e89f-4074-9843-d5c1f123878d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"9f5579a5-e89f-4074-9843-d5c1f123878d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"9f5579a5-e89f-4074-9843-d5c1f123878d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":610935,\"team_id\":2,\"email\":\"veselin.kulov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"9f5579a5-e89f-4074-9843-d5c1f123878d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":610935,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"9f5579a5-e89f-4074-9843-d5c1f123878d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610935} {\"correlation_id\":\"9f5579a5-e89f-4074-9843-d5c1f123878d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610935,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"9f5579a5-e89f-4074-9843-d5c1f123878d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":610935,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"9f5579a5-e89f-4074-9843-d5c1f123878d\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":610915,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"ee1e1cdc-f0b6-4353-b761-ada15282a954\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610915} {\"correlation_id\":\"ee1e1cdc-f0b6-4353-b761-ada15282a954\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610915,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"ee1e1cdc-f0b6-4353-b761-ada15282a954\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":610915,\"participants\":[{\"id\":997104,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":997105,\"user_id\":18,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"ee1e1cdc-f0b6-4353-b761-ada15282a954\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"ee1e1cdc-f0b6-4353-b761-ada15282a954\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"ee1e1cdc-f0b6-4353-b761-ada15282a954\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"ee1e1cdc-f0b6-4353-b761-ada15282a954\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"ee1e1cdc-f0b6-4353-b761-ada15282a954\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"ee1e1cdc-f0b6-4353-b761-ada15282a954\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"ee1e1cdc-f0b6-4353-b761-ada15282a954\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"ee1e1cdc-f0b6-4353-b761-ada15282a954\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":610915,\"team_id\":2,\"email\":\"veselin.kulov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"ee1e1cdc-f0b6-4353-b761-ada15282a954\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":610915,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"ee1e1cdc-f0b6-4353-b761-ada15282a954\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610915} {\"correlation_id\":\"ee1e1cdc-f0b6-4353-b761-ada15282a954\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610915,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"ee1e1cdc-f0b6-4353-b761-ada15282a954\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":610915,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"ee1e1cdc-f0b6-4353-b761-ada15282a954\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":610900,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"46958b32-a07f-4aa9-a052-c6a1e3bf70b2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610900} {\"correlation_id\":\"46958b32-a07f-4aa9-a052-c6a1e3bf70b2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610900,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"46958b32-a07f-4aa9-a052-c6a1e3bf70b2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":610900,\"participants\":[{\"id\":997081,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":997082,\"user_id\":18,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"46958b32-a07f-4aa9-a052-c6a1e3bf70b2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"46958b32-a07f-4aa9-a052-c6a1e3bf70b2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"46958b32-a07f-4aa9-a052-c6a1e3bf70b2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"46958b32-a07f-4aa9-a052-c6a1e3bf70b2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"46958b32-a07f-4aa9-a052-c6a1e3bf70b2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"46958b32-a07f-4aa9-a052-c6a1e3bf70b2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"46958b32-a07f-4aa9-a052-c6a1e3bf70b2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"46958b32-a07f-4aa9-a052-c6a1e3bf70b2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":610900,\"team_id\":2,\"email\":\"veselin.kulov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"46958b32-a07f-4aa9-a052-c6a1e3bf70b2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":610900,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"46958b32-a07f-4aa9-a052-c6a1e3bf70b2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610900} {\"correlation_id\":\"46958b32-a07f-4aa9-a052-c6a1e3bf70b2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610900,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"46958b32-a07f-4aa9-a052-c6a1e3bf70b2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":610900,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"46958b32-a07f-4aa9-a052-c6a1e3bf70b2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":610885,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"b9dc6bd0-a276-4d4c-ad70-8cd444119160\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610885} {\"correlation_id\":\"b9dc6bd0-a276-4d4c-ad70-8cd444119160\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610885,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"b9dc6bd0-a276-4d4c-ad70-8cd444119160\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":610885,\"participants\":[{\"id\":997051,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":997052,\"user_id\":18,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"b9dc6bd0-a276-4d4c-ad70-8cd444119160\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"b9dc6bd0-a276-4d4c-ad70-8cd444119160\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"b9dc6bd0-a276-4d4c-ad70-8cd444119160\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"b9dc6bd0-a276-4d4c-ad70-8cd444119160\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"b9dc6bd0-a276-4d4c-ad70-8cd444119160\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"b9dc6bd0-a276-4d4c-ad70-8cd444119160\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"b9dc6bd0-a276-4d4c-ad70-8cd444119160\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"b9dc6bd0-a276-4d4c-ad70-8cd444119160\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":610885,\"team_id\":2,\"email\":\"veselin.kulov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"b9dc6bd0-a276-4d4c-ad70-8cd444119160\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":610885,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"b9dc6bd0-a276-4d4c-ad70-8cd444119160\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610885} {\"correlation_id\":\"b9dc6bd0-a276-4d4c-ad70-8cd444119160\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610885,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"b9dc6bd0-a276-4d4c-ad70-8cd444119160\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":610885,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"b9dc6bd0-a276-4d4c-ad70-8cd444119160\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":610878,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"117202f1-4432-416d-922c-d7cd0f2ae201\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610878} {\"correlation_id\":\"117202f1-4432-416d-922c-d7cd0f2ae201\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610878,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"117202f1-4432-416d-922c-d7cd0f2ae201\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":610878,\"participants\":[{\"id\":997035,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":997036,\"user_id\":18,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"117202f1-4432-416d-922c-d7cd0f2ae201\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":391,\"provider\":\"office\",\"responseBody\":\"{\\\"error\\\":\\\"invalid_client\\\",\\\"error_description\\\":\\\"AADSTS7000215: Invalid client secret provided. Ensure the secret being sent in the request is the client secret value, not the client secret ID, for a secret added to app 'bbcbb2ef-6200-4fae-82bd-d81f5dd738da'. Trace ID: 72b211a8-6a7c-4954-b720-f1a47e981b00 Correlation ID: 5f9781fe-b28a-44ce-94e3-42c683cdfd60 Timestamp: 2026-05-11 10:43:40Z\\\",\\\"error_codes\\\":[7000215],\\\"timestamp\\\":\\\"2026-05-11 10:43:40Z\\\",\\\"trace_id\\\":\\\"72b211a8-6a7c-4954-b720-f1a47e981b00\\\",\\\"correlation_id\\\":\\\"5f9781fe-b28a-44ce-94e3-42c683cdfd60\\\",\\\"error_uri\\\":\\\"https://login.microsoftonline.com/error?code=7000215\\\"}\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:40] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":391,\"provider\":\"office\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"117202f1-4432-416d-922c-d7cd0f2ae201\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"117202f1-4432-416d-922c-d7cd0f2ae201\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"117202f1-4432-416d-922c-d7cd0f2ae201\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"117202f1-4432-416d-922c-d7cd0f2ae201\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1271,\"provider\":\"office\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1271,\"provider\":\"office\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:40] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"117202f1-4432-416d-922c-d7cd0f2ae201\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1271,\"provider\":\"office\",\"refreshToken\":\"118cde2c06993147b07ccaec4cbcd5026a819dea6c71081166a492933e392afb\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"117202f1-4432-416d-922c-d7cd0f2ae201\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"117202f1-4432-416d-922c-d7cd0f2ae201\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":610878,\"team_id\":2,\"email\":\"veselin.kulov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"117202f1-4432-416d-922c-d7cd0f2ae201\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":610878,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"117202f1-4432-416d-922c-d7cd0f2ae201\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610878} {\"correlation_id\":\"117202f1-4432-416d-922c-d7cd0f2ae201\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610878,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"117202f1-4432-416d-922c-d7cd0f2ae201\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":610878,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"117202f1-4432-416d-922c-d7cd0f2ae201\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Google Calendar] Failed to watch channel for calendar {\"calendarId\":\"a33076c1-8d97-431a-99f0-85c9524e118b\",\"code\":400,\"reason\":\"{\n \\\"error\\\": {\n \\\"errors\\\": [\n {\n \\\"domain\\\": \\\"global\\\",\n \\\"reason\\\": \\\"push.webhookUrlNotHttps\\\",\n \\\"message\\\": \\\"WebHook callback must be HTTPS: /webhook/calendar/google?resourceType=event\\\"\n }\n ],\n \\\"code\\\": 400,\n \\\"message\\\": \\\"WebHook callback must be HTTPS: /webhook/calendar/google?resourceType=event\\\"\n }\n}\"} {\"correlation_id\":\"0fdd7dce-04e2-4696-ba87-8089a739c81c\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:40] local.WARNING: [Calendar] Sync failed {\"calendarId\":\"a33076c1-8d97-431a-99f0-85c9524e118b\",\"code\":400,\"reason\":\"{\n \\\"error\\\": {\n \\\"errors\\\": [\n {\n \\\"domain\\\": \\\"global\\\",\n \\\"reason\\\": \\\"push.webhookUrlNotHttps\\\",\n \\\"message\\\": \\\"WebHook callback must be HTTPS: /webhook/calendar/google?resourceType=event\\\"\n }\n ],\n \\\"code\\\": 400,\n \\\"message\\\": \\\"WebHook callback must be HTTPS: /webhook/calendar/google?resourceType=event\\\"\n }\n}\"} {\"correlation_id\":\"0fdd7dce-04e2-4696-ba87-8089a739c81c\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":610874,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"6264f69a-8461-4d85-9725-ef4c30fb5d35\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610874} {\"correlation_id\":\"6264f69a-8461-4d85-9725-ef4c30fb5d35\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610874,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"6264f69a-8461-4d85-9725-ef4c30fb5d35\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":610874,\"participants\":[{\"id\":997025,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":997026,\"user_id\":18,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"6264f69a-8461-4d85-9725-ef4c30fb5d35\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"6264f69a-8461-4d85-9725-ef4c30fb5d35\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"6264f69a-8461-4d85-9725-ef4c30fb5d35\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"6264f69a-8461-4d85-9725-ef4c30fb5d35\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"6264f69a-8461-4d85-9725-ef4c30fb5d35\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"6264f69a-8461-4d85-9725-ef4c30fb5d35\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"6264f69a-8461-4d85-9725-ef4c30fb5d35\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"6264f69a-8461-4d85-9725-ef4c30fb5d35\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":610874,\"team_id\":2,\"email\":\"veselin.kulov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"6264f69a-8461-4d85-9725-ef4c30fb5d35\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":610874,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"6264f69a-8461-4d85-9725-ef4c30fb5d35\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610874} {\"correlation_id\":\"6264f69a-8461-4d85-9725-ef4c30fb5d35\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610874,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"6264f69a-8461-4d85-9725-ef4c30fb5d35\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":610874,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"6264f69a-8461-4d85-9725-ef4c30fb5d35\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":610867,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"978698d8-e33a-4d4a-b374-04919cae6ac2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610867} {\"correlation_id\":\"978698d8-e33a-4d4a-b374-04919cae6ac2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610867,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"978698d8-e33a-4d4a-b374-04919cae6ac2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":610867,\"participants\":[{\"id\":997011,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":997012,\"user_id\":18,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"978698d8-e33a-4d4a-b374-04919cae6ac2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"978698d8-e33a-4d4a-b374-04919cae6ac2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"978698d8-e33a-4d4a-b374-04919cae6ac2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"978698d8-e33a-4d4a-b374-04919cae6ac2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"978698d8-e33a-4d4a-b374-04919cae6ac2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"978698d8-e33a-4d4a-b374-04919cae6ac2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"978698d8-e33a-4d4a-b374-04919cae6ac2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"978698d8-e33a-4d4a-b374-04919cae6ac2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":610867,\"team_id\":2,\"email\":\"veselin.kulov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"978698d8-e33a-4d4a-b374-04919cae6ac2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":610867,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"978698d8-e33a-4d4a-b374-04919cae6ac2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610867} {\"correlation_id\":\"978698d8-e33a-4d4a-b374-04919cae6ac2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610867,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"978698d8-e33a-4d4a-b374-04919cae6ac2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":610867,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"978698d8-e33a-4d4a-b374-04919cae6ac2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":610764,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"3e294e41-3a19-4afe-87b3-4f7f0336107c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610764} {\"correlation_id\":\"3e294e41-3a19-4afe-87b3-4f7f0336107c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610764,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"3e294e41-3a19-4afe-87b3-4f7f0336107c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":610764,\"participants\":[{\"id\":996951,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":996952,\"user_id\":18,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"3e294e41-3a19-4afe-87b3-4f7f0336107c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"3e294e41-3a19-4afe-87b3-4f7f0336107c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"3e294e41-3a19-4afe-87b3-4f7f0336107c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"3e294e41-3a19-4afe-87b3-4f7f0336107c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"3e294e41-3a19-4afe-87b3-4f7f0336107c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"3e294e41-3a19-4afe-87b3-4f7f0336107c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"3e294e41-3a19-4afe-87b3-4f7f0336107c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"3e294e41-3a19-4afe-87b3-4f7f0336107c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":610764,\"team_id\":2,\"email\":\"veselin.kulov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"3e294e41-3a19-4afe-87b3-4f7f0336107c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":610764,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"3e294e41-3a19-4afe-87b3-4f7f0336107c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610764} {\"correlation_id\":\"3e294e41-3a19-4afe-87b3-4f7f0336107c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610764,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"3e294e41-3a19-4afe-87b3-4f7f0336107c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":610764,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"3e294e41-3a19-4afe-87b3-4f7f0336107c\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":610617,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"732b2bbb-ee7f-4bb2-85b1-e6478955544b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610617} {\"correlation_id\":\"732b2bbb-ee7f-4bb2-85b1-e6478955544b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610617,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"732b2bbb-ee7f-4bb2-85b1-e6478955544b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":610617,\"participants\":[{\"id\":996641,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":996642,\"user_id\":18,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"732b2bbb-ee7f-4bb2-85b1-e6478955544b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"732b2bbb-ee7f-4bb2-85b1-e6478955544b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"732b2bbb-ee7f-4bb2-85b1-e6478955544b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"732b2bbb-ee7f-4bb2-85b1-e6478955544b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"732b2bbb-ee7f-4bb2-85b1-e6478955544b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"732b2bbb-ee7f-4bb2-85b1-e6478955544b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"732b2bbb-ee7f-4bb2-85b1-e6478955544b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"732b2bbb-ee7f-4bb2-85b1-e6478955544b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":610617,\"team_id\":2,\"email\":\"veselin.kulov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"732b2bbb-ee7f-4bb2-85b1-e6478955544b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":610617,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"732b2bbb-ee7f-4bb2-85b1-e6478955544b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610617} {\"correlation_id\":\"732b2bbb-ee7f-4bb2-85b1-e6478955544b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610617,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"732b2bbb-ee7f-4bb2-85b1-e6478955544b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":610617,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"732b2bbb-ee7f-4bb2-85b1-e6478955544b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":610539,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"d94f0ac2-c007-4b7d-9fd3-27fc32f9b9a2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610539} {\"correlation_id\":\"d94f0ac2-c007-4b7d-9fd3-27fc32f9b9a2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610539,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"d94f0ac2-c007-4b7d-9fd3-27fc32f9b9a2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":610539,\"participants\":[{\"id\":996485,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":996486,\"user_id\":18,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"d94f0ac2-c007-4b7d-9fd3-27fc32f9b9a2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"d94f0ac2-c007-4b7d-9fd3-27fc32f9b9a2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"d94f0ac2-c007-4b7d-9fd3-27fc32f9b9a2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d94f0ac2-c007-4b7d-9fd3-27fc32f9b9a2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"d94f0ac2-c007-4b7d-9fd3-27fc32f9b9a2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"d94f0ac2-c007-4b7d-9fd3-27fc32f9b9a2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"d94f0ac2-c007-4b7d-9fd3-27fc32f9b9a2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"d94f0ac2-c007-4b7d-9fd3-27fc32f9b9a2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":610539,\"team_id\":2,\"email\":\"veselin.kulov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"d94f0ac2-c007-4b7d-9fd3-27fc32f9b9a2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":610539,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"d94f0ac2-c007-4b7d-9fd3-27fc32f9b9a2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610539} {\"correlation_id\":\"d94f0ac2-c007-4b7d-9fd3-27fc32f9b9a2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610539,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"d94f0ac2-c007-4b7d-9fd3-27fc32f9b9a2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":610539,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"d94f0ac2-c007-4b7d-9fd3-27fc32f9b9a2\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":610528,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"c316e103-4e48-4f28-8bc6-206714fc00ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610528} {\"correlation_id\":\"c316e103-4e48-4f28-8bc6-206714fc00ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610528,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"c316e103-4e48-4f28-8bc6-206714fc00ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":610528,\"participants\":[{\"id\":996463,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":996464,\"user_id\":18,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"c316e103-4e48-4f28-8bc6-206714fc00ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"c316e103-4e48-4f28-8bc6-206714fc00ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"c316e103-4e48-4f28-8bc6-206714fc00ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"c316e103-4e48-4f28-8bc6-206714fc00ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"c316e103-4e48-4f28-8bc6-206714fc00ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"c316e103-4e48-4f28-8bc6-206714fc00ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"c316e103-4e48-4f28-8bc6-206714fc00ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"c316e103-4e48-4f28-8bc6-206714fc00ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":610528,\"team_id\":2,\"email\":\"veselin.kulov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"c316e103-4e48-4f28-8bc6-206714fc00ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":610528,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"c316e103-4e48-4f28-8bc6-206714fc00ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610528} {\"correlation_id\":\"c316e103-4e48-4f28-8bc6-206714fc00ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610528,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"c316e103-4e48-4f28-8bc6-206714fc00ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":610528,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"c316e103-4e48-4f28-8bc6-206714fc00ec\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":610506,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"f83f62c6-544e-4551-8bc2-99f405d022e6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610506} {\"correlation_id\":\"f83f62c6-544e-4551-8bc2-99f405d022e6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610506,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"f83f62c6-544e-4551-8bc2-99f405d022e6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:40] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":610506,\"participants\":[{\"id\":996419,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":996420,\"user_id\":18,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"f83f62c6-544e-4551-8bc2-99f405d022e6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"f83f62c6-544e-4551-8bc2-99f405d022e6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"f83f62c6-544e-4551-8bc2-99f405d022e6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"f83f62c6-544e-4551-8bc2-99f405d022e6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"f83f62c6-544e-4551-8bc2-99f405d022e6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"f83f62c6-544e-4551-8bc2-99f405d022e6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"f83f62c6-544e-4551-8bc2-99f405d022e6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"f83f62c6-544e-4551-8bc2-99f405d022e6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1271,\"provider\":\"office\",\"responseBody\":\"{\\\"error\\\":\\\"invalid_client\\\",\\\"error_description\\\":\\\"AADSTS7000215: Invalid client secret provided. Ensure the secret being sent in the request is the client secret value, not the client secret ID, for a secret added to app 'bbcbb2ef-6200-4fae-82bd-d81f5dd738da'. Trace ID: 8addf5ba-9879-4822-b2bb-e5002b0d2200 Correlation ID: feece826-abbe-4c27-85d0-0cddd6b33c3b Timestamp: 2026-05-11 10:43:41Z\\\",\\\"error_codes\\\":[7000215],\\\"timestamp\\\":\\\"2026-05-11 10:43:41Z\\\",\\\"trace_id\\\":\\\"8addf5ba-9879-4822-b2bb-e5002b0d2200\\\",\\\"correlation_id\\\":\\\"feece826-abbe-4c27-85d0-0cddd6b33c3b\\\",\\\"error_uri\\\":\\\"https://login.microsoftonline.com/error?code=7000215\\\"}\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1271,\"provider\":\"office\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":610506,\"team_id\":2,\"email\":\"veselin.kulov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"f83f62c6-544e-4551-8bc2-99f405d022e6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":610506,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"f83f62c6-544e-4551-8bc2-99f405d022e6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610506} {\"correlation_id\":\"f83f62c6-544e-4551-8bc2-99f405d022e6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610506,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"f83f62c6-544e-4551-8bc2-99f405d022e6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1351,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":610506,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"f83f62c6-544e-4551-8bc2-99f405d022e6\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1351,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1351,\"provider\":\"google\",\"refreshToken\":\"4271d15b9e60a606439caddc68337f783e472c85b03dacff14d1b6dfded9051c\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":610497,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"854a8b20-3fc0-4ea6-a7c0-e0ddd599dd3b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610497} {\"correlation_id\":\"854a8b20-3fc0-4ea6-a7c0-e0ddd599dd3b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610497,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"854a8b20-3fc0-4ea6-a7c0-e0ddd599dd3b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":610497,\"participants\":[{\"id\":996401,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":996402,\"user_id\":18,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"854a8b20-3fc0-4ea6-a7c0-e0ddd599dd3b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1351,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Bad Request\"}} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1351,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"854a8b20-3fc0-4ea6-a7c0-e0ddd599dd3b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"854a8b20-3fc0-4ea6-a7c0-e0ddd599dd3b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"854a8b20-3fc0-4ea6-a7c0-e0ddd599dd3b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"854a8b20-3fc0-4ea6-a7c0-e0ddd599dd3b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1366,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountService] Token needs refreshing {\"socialAccountId\":1366,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"854a8b20-3fc0-4ea6-a7c0-e0ddd599dd3b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"854a8b20-3fc0-4ea6-a7c0-e0ddd599dd3b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountService] Refreshing token from provider {\"socialAccountId\":1366,\"provider\":\"google\",\"refreshToken\":\"ae21385059b2eebfd43f68aecd56eccd702a1aabb6598f1f7ab594ed8af491b4\",\"state\":\"full-refresh\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"854a8b20-3fc0-4ea6-a7c0-e0ddd599dd3b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":610497,\"team_id\":2,\"email\":\"veselin.kulov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"854a8b20-3fc0-4ea6-a7c0-e0ddd599dd3b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":610497,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"854a8b20-3fc0-4ea6-a7c0-e0ddd599dd3b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610497} {\"correlation_id\":\"854a8b20-3fc0-4ea6-a7c0-e0ddd599dd3b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610497,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"854a8b20-3fc0-4ea6-a7c0-e0ddd599dd3b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":610497,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"854a8b20-3fc0-4ea6-a7c0-e0ddd599dd3b\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":610490,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"bab4020c-8e0d-42eb-afdb-e6bab67d42c7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610490} {\"correlation_id\":\"bab4020c-8e0d-42eb-afdb-e6bab67d42c7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610490,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"bab4020c-8e0d-42eb-afdb-e6bab67d42c7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":610490,\"participants\":[{\"id\":996385,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":996386,\"user_id\":18,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"bab4020c-8e0d-42eb-afdb-e6bab67d42c7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"bab4020c-8e0d-42eb-afdb-e6bab67d42c7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"bab4020c-8e0d-42eb-afdb-e6bab67d42c7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"bab4020c-8e0d-42eb-afdb-e6bab67d42c7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"bab4020c-8e0d-42eb-afdb-e6bab67d42c7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"bab4020c-8e0d-42eb-afdb-e6bab67d42c7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"bab4020c-8e0d-42eb-afdb-e6bab67d42c7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"bab4020c-8e0d-42eb-afdb-e6bab67d42c7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":610490,\"team_id\":2,\"email\":\"veselin.kulov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"bab4020c-8e0d-42eb-afdb-e6bab67d42c7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":610490,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"bab4020c-8e0d-42eb-afdb-e6bab67d42c7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610490} {\"correlation_id\":\"bab4020c-8e0d-42eb-afdb-e6bab67d42c7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610490,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"bab4020c-8e0d-42eb-afdb-e6bab67d42c7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":610490,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"bab4020c-8e0d-42eb-afdb-e6bab67d42c7\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1366,\"provider\":\"google\",\"responseBody\":{\"error\":\"invalid_grant\",\"error_description\":\"Bad Request\"}} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountObserver] Saving model {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.ERROR: [SocialAccountService] Failed to refresh token {\"socialAccountId\":1366,\"provider\":\"google\",\"reason\":\"Flow refresh required.\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":610470,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"55223d65-4d62-42ac-994b-ae4c8f27cbef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1115,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610470} {\"correlation_id\":\"55223d65-4d62-42ac-994b-ae4c8f27cbef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1115,\"provider\":\"google\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610470,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"55223d65-4d62-42ac-994b-ae4c8f27cbef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":610470,\"participants\":[{\"id\":996369,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":996370,\"user_id\":18,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"55223d65-4d62-42ac-994b-ae4c8f27cbef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: Calendar sync job dispatched {\"calendar_id\":378} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1421,\"provider\":\"office\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1421,\"provider\":\"office\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"55223d65-4d62-42ac-994b-ae4c8f27cbef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"55223d65-4d62-42ac-994b-ae4c8f27cbef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"55223d65-4d62-42ac-994b-ae4c8f27cbef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: Calendar sync job dispatched {\"calendar_id\":504} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"55223d65-4d62-42ac-994b-ae4c8f27cbef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"55223d65-4d62-42ac-994b-ae4c8f27cbef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"55223d65-4d62-42ac-994b-ae4c8f27cbef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"55223d65-4d62-42ac-994b-ae4c8f27cbef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.NOTICE: Calendar sync end {\"retrieved_calendars\":31,\"processed_calendars\":3} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: Jiminny\\Console\\Commands\\Command::run Memory usage for command {\"command\":\"calendar:sync\",\"memoryBeforeCommandInMb\":60.0,\"memoryAfterCommandInMB\":62.0,\"memoryPeakBeforeCommandInMb\":99.727,\"memoryPeakAfterCommandInMB\":99.727} {\"correlation_id\":\"d912ddca-1ae3-48d9-bdc2-26f5b2e99773\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:41] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":610470,\"team_id\":2,\"email\":\"veselin.kulov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"55223d65-4d62-42ac-994b-ae4c8f27cbef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":610470,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"55223d65-4d62-42ac-994b-ae4c8f27cbef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610470} {\"correlation_id\":\"55223d65-4d62-42ac-994b-ae4c8f27cbef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610470,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"55223d65-4d62-42ac-994b-ae4c8f27cbef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":610470,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"55223d65-4d62-42ac-994b-ae4c8f27cbef\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":610462,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"0108a9c2-fb98-43fc-ad71-084c53b40936\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610462} {\"correlation_id\":\"0108a9c2-fb98-43fc-ad71-084c53b40936\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610462,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"0108a9c2-fb98-43fc-ad71-084c53b40936\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":610462,\"participants\":[{\"id\":996353,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":996354,\"user_id\":18,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"0108a9c2-fb98-43fc-ad71-084c53b40936\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"0108a9c2-fb98-43fc-ad71-084c53b40936\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"0108a9c2-fb98-43fc-ad71-084c53b40936\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"0108a9c2-fb98-43fc-ad71-084c53b40936\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"0108a9c2-fb98-43fc-ad71-084c53b40936\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"0108a9c2-fb98-43fc-ad71-084c53b40936\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"0108a9c2-fb98-43fc-ad71-084c53b40936\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"0108a9c2-fb98-43fc-ad71-084c53b40936\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":610462,\"team_id\":2,\"email\":\"veselin.kulov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"0108a9c2-fb98-43fc-ad71-084c53b40936\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":610462,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"0108a9c2-fb98-43fc-ad71-084c53b40936\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610462} {\"correlation_id\":\"0108a9c2-fb98-43fc-ad71-084c53b40936\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610462,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"0108a9c2-fb98-43fc-ad71-084c53b40936\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":610462,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"0108a9c2-fb98-43fc-ad71-084c53b40936\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":610451,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"d1fa1d3a-52aa-496e-8905-56f7157299b1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610451} {\"correlation_id\":\"d1fa1d3a-52aa-496e-8905-56f7157299b1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610451,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"d1fa1d3a-52aa-496e-8905-56f7157299b1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":610451,\"participants\":[{\"id\":996340,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":996341,\"user_id\":18,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"d1fa1d3a-52aa-496e-8905-56f7157299b1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"d1fa1d3a-52aa-496e-8905-56f7157299b1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"d1fa1d3a-52aa-496e-8905-56f7157299b1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"d1fa1d3a-52aa-496e-8905-56f7157299b1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"d1fa1d3a-52aa-496e-8905-56f7157299b1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"d1fa1d3a-52aa-496e-8905-56f7157299b1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"d1fa1d3a-52aa-496e-8905-56f7157299b1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"d1fa1d3a-52aa-496e-8905-56f7157299b1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":610451,\"team_id\":2,\"email\":\"veselin.kulov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"d1fa1d3a-52aa-496e-8905-56f7157299b1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":610451,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"d1fa1d3a-52aa-496e-8905-56f7157299b1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610451} {\"correlation_id\":\"d1fa1d3a-52aa-496e-8905-56f7157299b1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:41] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610451,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"d1fa1d3a-52aa-496e-8905-56f7157299b1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":610451,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"d1fa1d3a-52aa-496e-8905-56f7157299b1\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":610438,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"da3f7962-a65d-4bfe-80bf-c33b4ccd36f5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610438} {\"correlation_id\":\"da3f7962-a65d-4bfe-80bf-c33b4ccd36f5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610438,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"da3f7962-a65d-4bfe-80bf-c33b4ccd36f5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":610438,\"participants\":[{\"id\":996320,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":996321,\"user_id\":18,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"da3f7962-a65d-4bfe-80bf-c33b4ccd36f5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"da3f7962-a65d-4bfe-80bf-c33b4ccd36f5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"da3f7962-a65d-4bfe-80bf-c33b4ccd36f5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"da3f7962-a65d-4bfe-80bf-c33b4ccd36f5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"da3f7962-a65d-4bfe-80bf-c33b4ccd36f5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"da3f7962-a65d-4bfe-80bf-c33b4ccd36f5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"da3f7962-a65d-4bfe-80bf-c33b4ccd36f5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"da3f7962-a65d-4bfe-80bf-c33b4ccd36f5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":610438,\"team_id\":2,\"email\":\"veselin.kulov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"da3f7962-a65d-4bfe-80bf-c33b4ccd36f5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":610438,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"da3f7962-a65d-4bfe-80bf-c33b4ccd36f5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610438} {\"correlation_id\":\"da3f7962-a65d-4bfe-80bf-c33b4ccd36f5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610438,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"da3f7962-a65d-4bfe-80bf-c33b4ccd36f5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":610438,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"da3f7962-a65d-4bfe-80bf-c33b4ccd36f5\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":610426,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"da7b40c8-e233-4cf9-a133-31f12397ad10\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610426} {\"correlation_id\":\"da7b40c8-e233-4cf9-a133-31f12397ad10\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610426,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"da7b40c8-e233-4cf9-a133-31f12397ad10\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":610426,\"participants\":[{\"id\":996306,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":996307,\"user_id\":18,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"da7b40c8-e233-4cf9-a133-31f12397ad10\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"da7b40c8-e233-4cf9-a133-31f12397ad10\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"da7b40c8-e233-4cf9-a133-31f12397ad10\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"da7b40c8-e233-4cf9-a133-31f12397ad10\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"da7b40c8-e233-4cf9-a133-31f12397ad10\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"da7b40c8-e233-4cf9-a133-31f12397ad10\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"da7b40c8-e233-4cf9-a133-31f12397ad10\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"da7b40c8-e233-4cf9-a133-31f12397ad10\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":610426,\"team_id\":2,\"email\":\"veselin.kulov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"da7b40c8-e233-4cf9-a133-31f12397ad10\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":610426,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"da7b40c8-e233-4cf9-a133-31f12397ad10\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610426} {\"correlation_id\":\"da7b40c8-e233-4cf9-a133-31f12397ad10\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610426,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"da7b40c8-e233-4cf9-a133-31f12397ad10\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":610426,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"da7b40c8-e233-4cf9-a133-31f12397ad10\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":610403,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89}} {\"correlation_id\":\"4e1aa6ad-3e68-46fe-8129-19feb67cee5f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610403} {\"correlation_id\":\"4e1aa6ad-3e68-46fe-8129-19feb67cee5f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610403,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"4e1aa6ad-3e68-46fe-8129-19feb67cee5f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":610403,\"participants\":[{\"id\":996282,\"user_id\":null,\"contact_id\":null,\"lead_id\":null},{\"id\":996283,\"user_id\":18,\"contact_id\":null,\"lead_id\":null}]} {\"correlation_id\":\"4e1aa6ad-3e68-46fe-8129-19feb67cee5f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"4e1aa6ad-3e68-46fe-8129-19feb67cee5f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"4e1aa6ad-3e68-46fe-8129-19feb67cee5f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"4e1aa6ad-3e68-46fe-8129-19feb67cee5f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"4e1aa6ad-3e68-46fe-8129-19feb67cee5f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"4e1aa6ad-3e68-46fe-8129-19feb67cee5f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [Prospect match] cached empty result - no API calls, try next matching method {\"identifier_type\":\"email\",\"identifier\":\"support@staging.jiminny.com\"} {\"correlation_id\":\"4e1aa6ad-3e68-46fe-8129-19feb67cee5f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [Prospect match] Resolved company domain from email {\"email\":\"support@staging.jiminny.com\",\"domain\":\"jiminny.com\"} {\"correlation_id\":\"4e1aa6ad-3e68-46fe-8129-19feb67cee5f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":610403,\"team_id\":2,\"email\":\"veselin.kulov@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"4e1aa6ad-3e68-46fe-8129-19feb67cee5f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":610403,\"participants_processed\":2,\"exact_matches\":0,\"domain_matches\":1,\"best_match_found\":true} {\"correlation_id\":\"4e1aa6ad-3e68-46fe-8129-19feb67cee5f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610403} {\"correlation_id\":\"4e1aa6ad-3e68-46fe-8129-19feb67cee5f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610403,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"4e1aa6ad-3e68-46fe-8129-19feb67cee5f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":610403,\"remote_search\":true,\"lead_id\":null,\"contact_id\":null,\"account_id\":26,\"opportunity_id\":22,\"stage_id\":89} {\"correlation_id\":\"4e1aa6ad-3e68-46fe-8129-19feb67cee5f\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [MatchActivityCrmData] Starting CRM data matching {\"activity\":610400,\"remote_search\":true,\"set_configuration\":2,\"old_state\":{\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":350,\"stage_id\":34}} {\"correlation_id\":\"c137d466-9e9c-4af1-82c0-9f8639805989\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610400} {\"correlation_id\":\"c137d466-9e9c-4af1-82c0-9f8639805989\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610400,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"c137d466-9e9c-4af1-82c0-9f8639805989\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [MatchActivityCrmData] Participants old state {\"activity\":610400,\"participants\":[{\"id\":996275,\"user_id\":1460,\"contact_id\":null,\"lead_id\":null},{\"id\":996276,\"user_id\":206,\"contact_id\":null,\"lead_id\":null},{\"id\":996277,\"user_id\":null,\"contact_id\":4487,\"lead_id\":null}]} {\"correlation_id\":\"c137d466-9e9c-4af1-82c0-9f8639805989\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"c137d466-9e9c-4af1-82c0-9f8639805989\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"c137d466-9e9c-4af1-82c0-9f8639805989\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"c137d466-9e9c-4af1-82c0-9f8639805989\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":148,\"team_id\":2} {\"correlation_id\":\"c137d466-9e9c-4af1-82c0-9f8639805989\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":610400,\"team_id\":2,\"email\":\"aneliya.angelova@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"c137d466-9e9c-4af1-82c0-9f8639805989\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [CrmActivityService] Email domain belongs to the team, skipping crm lookup {\"activity_id\":610400,\"team_id\":2,\"email\":\"horen.kirazyan@jiminny.onmicrosoft.com\"} {\"correlation_id\":\"c137d466-9e9c-4af1-82c0-9f8639805989\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: ProspectCache - Searching DB for opportunity by owner {\"account_id\":244,\"contact_id\":4487,\"owner_id\":1460} {\"correlation_id\":\"c137d466-9e9c-4af1-82c0-9f8639805989\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: ProspectCache - Fallback DB opportunity search {\"account_id\":244,\"contact_id\":4487} {\"correlation_id\":\"c137d466-9e9c-4af1-82c0-9f8639805989\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: ProspectCache - Opportunity DB search results {\"account_id\":244,\"contact_id\":4487,\"opportunity_id\":350} {\"correlation_id\":\"c137d466-9e9c-4af1-82c0-9f8639805989\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [Prospect match] Cache / local search hit {\"identifier_type\":\"email\",\"identifier\":\"robinson@crusoe.com\"} {\"correlation_id\":\"c137d466-9e9c-4af1-82c0-9f8639805989\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [CrmActivityService] CRM matching completed {\"activity_id\":610400,\"participants_processed\":3,\"exact_matches\":1,\"domain_matches\":0,\"best_match_found\":true} {\"correlation_id\":\"c137d466-9e9c-4af1-82c0-9f8639805989\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [ EsUpdateTarget ] Update single target {\"target\":\"activities\",\"purpose\":\"searchable-observer-update\",\"entityId\":610400} {\"correlation_id\":\"c137d466-9e9c-4af1-82c0-9f8639805989\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [ AsyncUpdateElasticSearch ] Entity added to Redis list {\"entityType\":\"activities\",\"entityId\":610400,\"collectionKey\":\"activities-for-update-priority\",\"withPriority\":true} {\"correlation_id\":\"c137d466-9e9c-4af1-82c0-9f8639805989\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [MatchActivityCrmData] Successfully matched CRM data {\"activity\":610400,\"remote_search\":true,\"lead_id\":null,\"contact_id\":4487,\"account_id\":244,\"opportunity_id\":350,\"stage_id\":34} {\"correlation_id\":\"c137d466-9e9c-4af1-82c0-9f8639805989\",\"trace_id\":\"019221d5-59dc-42a0-9f99-b9a718ee399c\"}\n[2026-05-11 10:43:42] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1115,\"provider\":\"google\"} {\"correlation_id\":\"003a1645-a7b4-447f-8217-b1f109d902a1\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:42] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1115,\"provider\":\"google\"} {\"correlation_id\":\"003a1645-a7b4-447f-8217-b1f109d902a1\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:42] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"003a1645-a7b4-447f-8217-b1f109d902a1\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:42] local.INFO: [Calendar] Processing sync {\"calendarId\":\"2676cb6d-f86c-427e-bf78-591e388e3c1e\",\"from\":null,\"to\":null,\"delta\":\"CJ_x49O3jpIDEJ_x49O3jpIDGAUgw67KlwMow67KlwM=\",\"last_sync\":\"2026-01-19 07:48:40\",\"dateMode\":\"daily\"} {\"correlation_id\":\"003a1645-a7b4-447f-8217-b1f109d902a1\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:42] local.WARNING: [Pipedrive] Account not connected for user {\"userId\":\"e6538737-e7b4-455f-a37a-3e79b665a220\",\"account\":{\"Jiminny\\\\Models\\\\SocialAccount\":{\"id\":1116,\"sociable_id\":241,\"provider_user_id\":\"19555731\",\"expires\":1775683749,\"refresh_token_expires\":null,\"provider\":\"pipedrive\",\"state\":\"full-refresh\",\"auth_scope\":\"base,deals:full,activities:full,contacts:full,search:read\",\"retry_after\":null,\"created_at\":\"2023-09-08 09:44:29\",\"updated_at\":\"2026-04-08 22:58:34\"}}} {\"correlation_id\":\"003a1645-a7b4-447f-8217-b1f109d902a1\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:42] local.INFO: [CrmOwnerResolver] Integration owner is not connected, attempting team members {\"crm_provider\":\"pipedrive\",\"crm_owner\":241,\"team_id\":19} {\"correlation_id\":\"003a1645-a7b4-447f-8217-b1f109d902a1\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:42] local.INFO: [CrmOwnerResolver] No team members found with active crm connection {\"crm_provider\":\"pipedrive\",\"team_id\":19} {\"correlation_id\":\"003a1645-a7b4-447f-8217-b1f109d902a1\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:42] local.INFO: [CrmOwnerResolver] No team member found with active crm connection {\"crm_provider\":\"pipedrive\",\"team_id\":19} {\"correlation_id\":\"003a1645-a7b4-447f-8217-b1f109d902a1\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:42] local.WARNING: [Calendar] CRM disconnected for user so events will not be matched {\"provider\":\"pipedrive\",\"user_id\":241,\"message\":\"Your Pipedrive account has become disconnected. Please login to Jiminny to reconnect.\"} {\"correlation_id\":\"003a1645-a7b4-447f-8217-b1f109d902a1\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:42] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1115,\"provider\":\"google\"} {\"correlation_id\":\"003a1645-a7b4-447f-8217-b1f109d902a1\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:42] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1115,\"provider\":\"google\"} {\"correlation_id\":\"003a1645-a7b4-447f-8217-b1f109d902a1\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:42] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"003a1645-a7b4-447f-8217-b1f109d902a1\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:42] local.INFO: [Google Calendar] Failed to watch channel for calendar {\"calendarId\":\"2676cb6d-f86c-427e-bf78-591e388e3c1e\",\"code\":400,\"reason\":\"{\n \\\"error\\\": {\n \\\"errors\\\": [\n {\n \\\"domain\\\": \\\"global\\\",\n \\\"reason\\\": \\\"push.webhookUrlNotHttps\\\",\n \\\"message\\\": \\\"WebHook callback must be HTTPS: /webhook/calendar/google?resourceType=event\\\"\n }\n ],\n \\\"code\\\": 400,\n \\\"message\\\": \\\"WebHook callback must be HTTPS: /webhook/calendar/google?resourceType=event\\\"\n }\n}\"} {\"correlation_id\":\"003a1645-a7b4-447f-8217-b1f109d902a1\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:42] local.WARNING: [Calendar] Sync failed {\"calendarId\":\"2676cb6d-f86c-427e-bf78-591e388e3c1e\",\"code\":400,\"reason\":\"{\n \\\"error\\\": {\n \\\"errors\\\": [\n {\n \\\"domain\\\": \\\"global\\\",\n \\\"reason\\\": \\\"push.webhookUrlNotHttps\\\",\n \\\"message\\\": \\\"WebHook callback must be HTTPS: /webhook/calendar/google?resourceType=event\\\"\n }\n ],\n \\\"code\\\": 400,\n \\\"message\\\": \\\"WebHook callback must be HTTPS: /webhook/calendar/google?resourceType=event\\\"\n }\n}\"} {\"correlation_id\":\"003a1645-a7b4-447f-8217-b1f109d902a1\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:42] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1421,\"provider\":\"office\"} {\"correlation_id\":\"f66a7935-f1a6-4d93-b53d-1464e6e79019\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:42] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1421,\"provider\":\"office\"} {\"correlation_id\":\"f66a7935-f1a6-4d93-b53d-1464e6e79019\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:42] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"f66a7935-f1a6-4d93-b53d-1464e6e79019\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:42] local.INFO: [Calendar] Processing sync {\"calendarId\":\"9e8b1a2c-1a8f-42bd-b161-810fc0baf540\",\"from\":null,\"to\":null,\"delta\":\"R0usmcdvmMuZCBYV0hguCLlvcOB4kXlhlC7KgH1SnZwTrZ3faZv1fXPQqJhxe_L9AxWWlb-wASsjGiiWlhsBUg9MFb3ZdlAYerVV_ZirRPbsKWCxEXhybD90arJmok_M4ecGFUQ9_BIGu-c6RAnJy2TRKZ7gPTsJi_8TGceGAuqimlhm4G4mjDLvYVVwImjjU7M3xJvUzL47dLOGNTJCww.k1TST0VEYCgbFOkwa3ysYMi100FtVfkzfqlXLnV6gPg\",\"last_sync\":\"2026-05-11 06:13:36\",\"dateMode\":\"daily\"} {\"correlation_id\":\"f66a7935-f1a6-4d93-b53d-1464e6e79019\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:42] local.INFO: [SocialAccountService] Fetching token {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"f66a7935-f1a6-4d93-b53d-1464e6e79019\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:42] local.INFO: [SocialAccountService] Token retrieved {\"socialAccountId\":1499,\"provider\":\"hubspot\"} {\"correlation_id\":\"f66a7935-f1a6-4d93-b53d-1464e6e79019\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:42] local.INFO: [EncryptedTokenManager] Generating access token. {\"mode\":\"legacy\"} {\"correlation_id\":\"f66a7935-f1a6-4d93-b53d-1464e6e79019\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:42] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {\"crm_provider\":\"hubspot\",\"crm_owner\":89,\"team_id\":2} {\"correlation_id\":\"f66a7935-f1a6-4d93-b53d-1464e6e79019\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}\n[2026-05-11 10:43:42] local.INFO: [MS Office Calendar] Skipping delta sync for daily mode {\"calendarId\":\"9e8b1a2c-1a8f-42bd-b161-810fc0baf540\"} {\"correlation_id\":\"f66a7935-f1a6-4d93-b53d-1464e6e79019\",\"trace_id\":\"34f632d7-cbf6-45bd-b635-4e176bb23db0\"}","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}]...
|
879150276765577717
|
5585982875328481636
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
JY-20725-handle-HS-search Project: faVsco.js, menu
JY-20725-handle-HS-search-rate-limit, menu
Start Listening for PHP Debug Connections
HandleHubspotRateLimitTest
Run 'HandleHubspotRateLimitTest'
Debug 'HandleHubspotRateLimitTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Sync Changes
Hide This Notification
Code changed:
Hide
3
70
3
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\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 Illuminate\Support\Facades\Redis;
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)
{
$cacheKey = $this->getRateLimitCacheKey();
$cachedRetryAfter = Redis::get($cacheKey);
if (is_string($cachedRetryAfter) && is_numeric($cachedRetryAfter)) {
throw new RateLimitException(
'Hubspot rate limit (cached circuit-breaker)',
(int) $cachedRetryAfter,
);
}
try {
return $apiCall();
} catch (Throwable $e) {
if ($this->isHubspotRateLimit($e)) {
$retryAfter = $this->parseRetryAfter($e);
Redis::setex($cacheKey, $retryAfter, (string) $retryAfter);
$this->log->warning('[Hubspot] Received 429 from API', [
'team_id' => $this->config->team_id,
'config_id' => $this->config->getId(),
'retry_after' => $retryAfter,
'policy' => $this->parsePolicy($e),
'reason' => $e->getMessage(),
]);
throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
}
throw $e;
}
}
private function getRateLimitCacheKey(): string
{
return sprintf('hubspot:ratelimit:portal:%d', $this->config->getId());
}
public function isHubspotRateLimit(Throwable $e): bool
{
if ($e instanceof BadRequest
|| $e instanceof DealApiException
|| $e instanceof ContactApiException
|| $e instanceof CompanyApiException
|| $e instanceof \GuzzleHttp\Exception\RequestException
) {
return (int) $e->getCode() === 429;
}
return false;
}
public function parseRetryAfter(Throwable $e): int
{
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$e ' . PHP_EOL . print_r($e, true));
return 5;
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;
}
}
$policy = $this->parsePolicy($e);
if ($policy === 'TEN_SECONDLY_ROLLING') {
return 10;
}
if ($policy === 'SECONDLY') {
return 1;
}
if ($policy === 'DAILY_LIMIT') {
return 600;
}
$this->log->warning('[Hubspot] No retry-after header or policy name found, using default', [
'exception_class' => get_class($e),
]);
return 10;
}
public function parsePolicy(Throwable $e): ?string
{
if (! method_exists($e, 'getResponseBody')) {
return null;
}
$body = $e->getResponseBody();
if (is_string($body)) {
$body = json_decode($body, true) ?? [];
}
if (! is_array($body)) {
return null;
}
$policy = $body['policyName'] ?? $body['policy'] ?? $body['context']['policyName'] ?? null;
return is_string($policy) ? strtoupper($policy) : null;
}
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
);
}
/**
* Execute a search request against HubSpot CRM objects with rate limiting.
*
* @param string $objectType The object type ('deals', 'companies', 'contacts', 'calls')
* @param array<string, mixed> $payload The search payload with filters, sorts, properties, etc.
* @return array The search response with 'results', 'total', 'paging' keys
* @throws RateLimitException When rate limit is hit
* @throws HubspotException On API errors
*/
public function search(string $objectType, array $payload): array
{
$endpoint = self::BASE_URL . "/crm/v3/objects/{$objectType}/search";
return $this->executeRequest(function () use ($endpoint, $payload) {
$response = $this->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$response ' . PHP_EOL . print_r($response, true));
\Illuminate\Support\Facades\Log::channel('custom_channel')->info('$headers ' . PHP_EOL . print_r($response->getHeaders(), true));
return $response->toArray();
});
}
/**
* @throws DealApiException
* @throws CrmException
*/
public function getOpportunityById(string $crmId, array $fields): array
{
try {
// $deal = $this->executeRequest(fn () => $this->getNewInstance()->crm()->deals()->basicApi()->getById(
$deal = $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') {
return $this->getInstance()->getClient()?->request(
method: $method,
endpoint: $endpoint,
query_string: $queryString
);
} else {
return $this->getInstance()->getClient()->request($method, $endpoint, [
'json' => ($payload),
]);
}
}
/**
* @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 (RateLimitException $e) {
// throw $e;
} 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);
}
}
Show Replace Field
Search History
Received 429 from API
New Line
Match Case
Words
Regex
Replace History
Replace
New Line
Preserve case
0 results
Previous Occurrence
Next Occurrence
Filter Search Results
Open in Window, Multiple Cursors
Click to highlight
Close
Sync Changes
Hide This Notification
Code changed:
Hide
[2026-05-11 10:41:16] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"mailbox:batch:process","memoryBeforeCommandInMb":60.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"ee9b9096-b1f1-4e2b-ac17-af0003eb00c0","trace_id":"8904c61b-d5b4-44cc-91bf-68f0f73222c0"}
[2026-05-11 10:41:16] local.INFO: [EmailSchedule] STARTING batch process {"host":"docker_lamp_1"} {"correlation_id":"ee9b9096-b1f1-4e2b-ac17-af0003eb00c0","trace_id":"8904c61b-d5b4-44cc-91bf-68f0f73222c0"}
[2026-05-11 10:41:17] local.INFO: [EmailSchedule] FINISHED batch process {"host":"docker_lamp_1","processed":0} {"correlation_id":"ee9b9096-b1f1-4e2b-ac17-af0003eb00c0","trace_id":"8904c61b-d5b4-44cc-91bf-68f0f73222c0"}
[2026-05-11 10:41:17] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"mailbox:batch:process","memoryBeforeCommandInMb":60.0,"memoryAfterCommandInMB":60.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"ee9b9096-b1f1-4e2b-ac17-af0003eb00c0","trace_id":"8904c61b-d5b4-44cc-91bf-68f0f73222c0"}
[2026-05-11 10:41:21] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"crm:sync-hubspot-objects","memoryBeforeCommandInMb":60.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"13c8d2de-a598-41db-b4ca-9799a4bee946","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:21] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"crm:sync-hubspot-objects","memoryBeforeCommandInMb":60.0,"memoryAfterCommandInMB":60.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"13c8d2de-a598-41db-b4ca-9799a4bee946","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:21] local.INFO: [SyncHubspotObjects] Starting sync {"team":"b2b115eb-93ce-4d1b-929c-173757df8fba","usage":24894088,"real_usage":65011712,"pid":62058} {"correlation_id":"81433689-3fcd-4128-8b21-068ce5ee75be","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:21] local.WARNING: [HubSpot] Account not connected for user {"userId":"33e34a7a-1c02-4f04-87ac-22c3a385e6e3","account":{"Jiminny\\Models\\SocialAccount":{"id":306,"sociable_id":109,"provider_user_id":"11348452","expires":1701077403,"refresh_token_expires":null,"provider":"hubspot","state":"full-refresh","auth_scope":null,"retry_after":null,"created_at":"2020-09-01 16:59:04","updated_at":"2023-11-27 09:30:03"}}} {"correlation_id":"81433689-3fcd-4128-8b21-068ce5ee75be","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:21] local.INFO: [CrmOwnerResolver] Integration owner is not connected, attempting team members {"crm_provider":"hubspot","crm_owner":109,"team_id":29} {"correlation_id":"81433689-3fcd-4128-8b21-068ce5ee75be","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:21] local.INFO: [CrmOwnerResolver] No team members found with active crm connection {"crm_provider":"hubspot","team_id":29} {"correlation_id":"81433689-3fcd-4128-8b21-068ce5ee75be","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:21] local.INFO: [CrmOwnerResolver] No team member found with active crm connection {"crm_provider":"hubspot","team_id":29} {"correlation_id":"81433689-3fcd-4128-8b21-068ce5ee75be","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:21] local.INFO: [SyncHubspotObjects] Sync finished {"team":"b2b115eb-93ce-4d1b-929c-173757df8fba","provider":"hubspot","status":"disconnected","duration_ms":35.85,"usage":24956128,"real_usage":65011712,"pid":62058,"reason":"Your HubSpot account has become disconnected. Please login to Jiminny to reconnect."} {"correlation_id":"81433689-3fcd-4128-8b21-068ce5ee75be","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:21] local.INFO: [SyncHubspotObjects] Starting sync {"team":"abae74b8-bfa8-4383-9a7f-89f4bf2bdbb4","usage":24914344,"real_usage":65011712,"pid":62058} {"correlation_id":"d2cb4aff-14c0-474f-89f2-f5c69dafe934","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:21] local.INFO: [SocialAccountService] Fetching token {"socialAccountId":1499,"provider":"hubspot"} {"correlation_id":"d2cb4aff-14c0-474f-89f2-f5c69dafe934","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:21] local.INFO: [SocialAccountService] Token retrieved {"socialAccountId":1499,"provider":"hubspot"} {"correlation_id":"d2cb4aff-14c0-474f-89f2-f5c69dafe934","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:21] local.INFO: [EncryptedTokenManager] Generating access token. {"mode":"legacy"} {"correlation_id":"d2cb4aff-14c0-474f-89f2-f5c69dafe934","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:21] local.INFO: [CrmOwnerResolver] Integration owner matched as CRM Owner {"crm_provider":"hubspot","crm_owner":148,"team_id":2} {"correlation_id":"d2cb4aff-14c0-474f-89f2-f5c69dafe934","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:21] local.INFO: [HubSpot] Syncing opportunities using strategy: lastModified {"team":2} {"correlation_id":"d2cb4aff-14c0-474f-89f2-f5c69dafe934","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:21] local.INFO: [Hubspot] Pagination completed {"team_id":2,"endpoint":"https://api.hubapi.com/crm/v3/objects/deals/search","total_requests":1,"total_records_fetched":0,"total_elapsed_seconds":0.26,"average_seconds_per_request":0.26} {"correlation_id":"d2cb4aff-14c0-474f-89f2-f5c69dafe934","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:21] local.INFO: [HubSpot] Synced opportunities {"team":2,"strategies":"lastModified","sync_count":0,"total":0,"last_synced_id":null,"duration_ms":288.63} {"correlation_id":"d2cb4aff-14c0-474f-89f2-f5c69dafe934","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:21] local.INFO: [SyncHubspotObjects] Sync finished {"team":"abae74b8-bfa8-4383-9a7f-89f4bf2bdbb4","provider":"hubspot","status":"completed","duration_ms":324.99,"usage":24978696,"real_usage":65011712,"pid":62058} {"correlation_id":"d2cb4aff-14c0-474f-89f2-f5c69dafe934","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:22] local.INFO: [SyncHubspotObjects] Starting sync {"team":"c6b9d6b0-b48d-4832-a68c-a57d60651888","usage":24956624,"real_usage":65011712,"pid":62058} {"correlation_id":"955d9400-2f7f-4170-be31-43bafa1ef3bd","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:22] local.WARNING: [HubSpot] Account not connected for user {"userId":"71e3aac5-fb66-47c5-a236-2d051ae3e319","account":null} {"correlation_id":"955d9400-2f7f-4170-be31-43bafa1ef3bd","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:22] local.INFO: [CrmOwnerResolver] Integration owner is not connected, attempting team members {"crm_provider":"hubspot","crm_owner":256,"team_id":49} {"correlation_id":"955d9400-2f7f-4170-be31-43bafa1ef3bd","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:22] local.INFO: [CrmOwnerResolver] No team members found with active crm connection {"crm_provider":"hubspot","team_id":49} {"correlation_id":"955d9400-2f7f-4170-be31-43bafa1ef3bd","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:22] local.INFO: [CrmOwnerResolver] No team member found with active crm connection {"crm_provider":"hubspot","team_id":49} {"correlation_id":"955d9400-2f7f-4170-be31-43bafa1ef3bd","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:22] local.INFO: [SyncHubspotObjects] Sync finished {"team":"c6b9d6b0-b48d-4832-a68c-a57d60651888","provider":"hubspot","status":"disconnected","duration_ms":17.51,"usage":24941048,"real_usage":65011712,"pid":62058,"reason":"Social account for HubSpot cannot be found. Please login to Jiminny to connect."} {"correlation_id":"955d9400-2f7f-4170-be31-43bafa1ef3bd","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:22] local.INFO: [SyncHubspotObjects] Starting sync {"team":"b2d49a54-b645-4637-a7ae-a86cfce6e8e4","usage":24901688,"real_usage":65011712,"pid":62058} {"correlation_id":"6d0b5717-9160-4c2f-ad12-56e42a2c544a","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:22] local.WARNING: [HubSpot] Account not connected for user {"userId":"2ac0447f-3c8c-4ce0-baeb-b63ddb76fa9b","account":null} {"correlation_id":"6d0b5717-9160-4c2f-ad12-56e42a2c544a","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:22] local.INFO: [CrmOwnerResolver] Integration owner is not connected, attempting team members {"crm_provider":"hubspot","crm_owner":130,"team_id":42} {"correlation_id":"6d0b5717-9160-4c2f-ad12-56e42a2c544a","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:22] local.INFO: [CrmOwnerResolver] No team members found with active crm connection {"crm_provider":"hubspot","team_id":42} {"correlation_id":"6d0b5717-9160-4c2f-ad12-56e42a2c544a","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:22] local.INFO: [CrmOwnerResolver] No team member found with active crm connection {"crm_provider":"hubspot","team_id":42} {"correlation_id":"6d0b5717-9160-4c2f-ad12-56e42a2c544a","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:22] local.INFO: [SyncHubspotObjects] Sync finished {"team":"b2d49a54-b645-4637-a7ae-a86cfce6e8e4","provider":"hubspot","status":"disconnected","duration_ms":10.99,"usage":24944632,"real_usage":65011712,"pid":62058,"reason":"Social account for HubSpot cannot be found. Please login to Jiminny to connect."} {"correlation_id":"6d0b5717-9160-4c2f-ad12-56e42a2c544a","trace_id":"85901fdb-28fb-4611-bb3c-85a8bc158a75"}
[2026-05-11 10:41:39] local.INFO: [HubSpot Journal Polling] No data {"correlation_id":"1f8f11e6-0cf5-49dd-b8d7-56f51ec27f8e","trace_id":"0f08076e-448d-487e-99a0-e73dae5c0ad4"}
[2026-05-11 10:41:39] local.WARNING: [HubSpot Journal Polling] Maximum empty results reached, stopping {"empty_results":5,"max_empty_results":5} {"correlation_id":"1f8f11e6-0cf5-49dd-b8d7-56f51ec27f8e","trace_id":"0f08076e-448d-487e-99a0-e73dae5c0ad4"}
[2026-05-11 10:41:39] local.WARNING: [HubSpot Journal Polling] Maximum empty results reached, stopping {"empty_results":5,"max_empty_results":5} {"correlation_id":"1f8f11e6-0cf5-49dd-b8d7-56f51ec27f8e","trace_id":"0f08076e-448d-487e-99a0-e73dae5c0ad4"}
[2026-05-11 10:41:39] local.INFO: [HubSpot Journal Polling] Service ending {"runtime_seconds":57,"total_cycles":5,"files_downloaded":0,"empty_files":0,"other_portal_skipped":0,"total_events":0,"events_per_file":0,"avg_api_ms":296.8,"avg_download_ms":0.0,"avg_transform_ms":0.0,"avg_process_ms":0.0,"peak_memory_mb":99.73} {"correlation_id":"1f8f11e6-0cf5-49dd-b8d7-56f51ec27f8e","trace_id":"0f08076e-448d-487e-99a0-e73dae5c0ad4"}
[2026-05-11 10:41:39] local.INFO: [HubSpot Journal Polling] Saved offset to database on cleanup {"offset":"019e15a9-9ea0-7da7-87bc-82592e3ccf0d"} {"correlation_id":"1f8f11e6-0cf5-49dd-b8d7-56f51ec27f8e","trace_id":"0f08076e-448d-487e-99a0-e73dae5c0ad4"}
[2026-05-11 10:41:39] local.INFO: [HubSpot Journal Polling] Released polling lock {"correlation_id":"1f8f11e6-0cf5-49dd-b8d7-56f51ec27f8e","trace_id":"0f08076e-448d-487e-99a0-e73dae5c0ad4"}
[2026-05-11 10:42:07] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"meeting-bot:schedule-bot","memoryBeforeCommandInMb":60.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"1aa66921-0a71-4d61-8cd7-0663242dca44","trace_id":"f6b1d739-39b6-4043-a81e-a1af2c76d401"}
[2026-05-11 10:42:07] local.INFO: [ScheduleBotCommand] Number of activities to be captured: 0 {"correlation_id":"1aa66921-0a71-4d61-8cd7-0663242dca44","trace_id":"f6b1d739-39b6-4043-a81e-a1af2c76d401"}
[2026-05-11 10:42:07] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"meeting-bot:schedule-bot","memoryBeforeCommandInMb":60.0,"memoryAfterCommandInMB":60.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"1aa66921-0a71-4d61-8cd7-0663242dca44","trace_id":"f6b1d739-39b6-4043-a81e-a1af2c76d401"}
[2026-05-11 10:42:10] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"dialers:monitor-activities","memoryBeforeCommandInMb":60.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"cc420ba5-39bf-4183-95ae-48962a9c124b","trace_id":"8daf6081-5c8a-4fae-98d8-e317d8580bc3"}
[2026-05-11 10:42:10] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"dialers:monitor-activities","memoryBeforeCommandInMb":60.0,"memoryAfterCommandInMB":60.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"cc420ba5-39bf-4183-95ae-48962a9c124b","trace_id":"8daf6081-5c8a-4fae-98d8-e317d8580bc3"}
[2026-05-11 10:42:13] local.NOTICE: Monitoring start {"correlation_id":"50696062-587c-4c43-8075-7229916ea46f","trace_id":"b58f754c-906f-413b-80e6-f89189dbe078"}
[2026-05-11 10:42:13] local.NOTICE: Monitoring end {"correlation_id":"50696062-587c-4c43-8075-7229916ea46f","trace_id":"b58f754c-906f-413b-80e6-f89189dbe078"}
[2026-05-11 10:42:17] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"mailbox:skip-lists:refresh","memoryBeforeCommandInMb":60.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"f70656cd-6de3-4f8b-bb9d-74a941c552c4","trace_id":"c780d8e3-2abd-41a3-a9dc-7c2be6aa22d4"}
[2026-05-11 10:42:17] local.INFO: Jiminny\Console\Commands\Command::run Memory usage for command {"command":"mailbox:skip-lists:refresh","memoryBeforeCommandInMb":60.0,"memoryAfterCommandInMB":60.0,"memoryPeakBeforeCommandInMb":99.727,"memoryPeakAfterCommandInMB":99.727} {"correlation_id":"f70656cd-6de3-4f8b-bb9d-74a941c552c4","trace_id":"c780d8e3-2abd-41a3-a9dc-7c2be6aa22d4"}
[2026-05-11 10:42:20] local.INFO: Jiminny\Console\Commands\Command::run Memory usage before starting command {"command":"mailbox:batch:process","memoryBeforeCommandInMb":60.0,"memoryPeakBeforeCommandInMb":99.727} {"correlation_id":"b2e73601-1777-4ddb-a152-fe85f4c89afc","trace_id":"8bac5054-216a-4605-b13b-9efab8ab5341"}
[2026-05-11 10:42:20] local.INFO: [EmailSchedule] STARTING batch process {"host":"docker_lamp_1"} {"correlation_id":"b2e73601-1777-4ddb-a152-fe85f4c89afc","trace_id":"8bac5054-216a-4605-b13b-9efab8ab5341"}
[2026-05-11 10:42:20] local.INFO: [EmailSchedule] FINISHED batch process {"host":"docker_lamp_1","processed":0} {"correlation_i...
|
17971
|
NULL
|
NULL
|
NULL
|